Initial commit (users and statuses)
This commit is contained in:
		
						commit
						d77dcf62b4
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| *.psql | ||||
| *.sqlite3 | ||||
							
								
								
									
										37
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								.pre-commit-config.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| repos: | ||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||
|     rev: v3.3.0 | ||||
|     hooks: | ||||
|       - id: check-case-conflict | ||||
|       - id: check-merge-conflict | ||||
|       - id: check-yaml | ||||
|       - id: end-of-file-fixer | ||||
|       - id: file-contents-sorter | ||||
|         args: ["--ignore-case"] | ||||
|         files: "^.gitignore$" | ||||
|       - id: mixed-line-ending | ||||
|         args: ["--fix=lf"] | ||||
|       - id: trailing-whitespace | ||||
|       - id: pretty-format-json | ||||
| 
 | ||||
|   - repo: https://github.com/psf/black | ||||
|     rev: 22.10.0 | ||||
|     hooks: | ||||
|       - id: black | ||||
|         args: ["--target-version=py37"] | ||||
| 
 | ||||
|   - repo: https://github.com/pycqa/isort | ||||
|     rev: 5.10.1 | ||||
|     hooks: | ||||
|       - id: isort | ||||
|         args: ["--profile=black"] | ||||
| 
 | ||||
|   - repo: https://gitlab.com/pycqa/flake8 | ||||
|     rev: 5.0.4 | ||||
|     hooks: | ||||
|       - id: flake8 | ||||
| 
 | ||||
|   - repo: https://github.com/pre-commit/mirrors-mypy | ||||
|     rev: v0.982 | ||||
|     hooks: | ||||
|       - id: mypy | ||||
							
								
								
									
										0
									
								
								core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										6
									
								
								core/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								core/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class CoreConfig(AppConfig): | ||||
|     default_auto_field = "django.db.models.BigAutoField" | ||||
|     name = "core" | ||||
							
								
								
									
										3
									
								
								core/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								core/config.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| class Config: | ||||
| 
 | ||||
|     pass | ||||
							
								
								
									
										5
									
								
								core/context.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								core/context.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| from django.conf import settings | ||||
| 
 | ||||
| 
 | ||||
| def config_context(request): | ||||
|     return {"config": {"site_name": settings.SITE_NAME}} | ||||
							
								
								
									
										11
									
								
								core/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								core/forms.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| from crispy_forms.helper import FormHelper as BaseFormHelper | ||||
| from crispy_forms.layout import Submit | ||||
| 
 | ||||
| 
 | ||||
| class FormHelper(BaseFormHelper): | ||||
| 
 | ||||
|     submit_text = "Submit" | ||||
| 
 | ||||
|     def __init__(self, form=None, submit_text=None): | ||||
|         super().__init__(form) | ||||
|         self.add_input(Submit("submit", submit_text or "Submit")) | ||||
							
								
								
									
										21
									
								
								core/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								core/views.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| from django.views.generic import TemplateView | ||||
| 
 | ||||
| from statuses.views.home import Home | ||||
| from users.models import Identity | ||||
| 
 | ||||
| 
 | ||||
| def homepage(request): | ||||
|     if request.user.is_authenticated: | ||||
|         return Home.as_view()(request) | ||||
|     else: | ||||
|         return LoggedOutHomepage.as_view()(request) | ||||
| 
 | ||||
| 
 | ||||
| class LoggedOutHomepage(TemplateView): | ||||
| 
 | ||||
|     template_name = "index.html" | ||||
| 
 | ||||
|     def get_context_data(self): | ||||
|         return { | ||||
|             "identities": Identity.objects.filter(local=True), | ||||
|         } | ||||
							
								
								
									
										22
									
								
								manage.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										22
									
								
								manage.py
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,22 @@ | ||||
| #!/usr/bin/env python | ||||
| """Django's command-line utility for administrative tasks.""" | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     """Run administrative tasks.""" | ||||
|     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "takahe.settings") | ||||
|     try: | ||||
|         from django.core.management import execute_from_command_line | ||||
|     except ImportError as exc: | ||||
|         raise ImportError( | ||||
|             "Couldn't import Django. Are you sure it's installed and " | ||||
|             "available on your PYTHONPATH environment variable? Did you " | ||||
|             "forget to activate a virtual environment?" | ||||
|         ) from exc | ||||
|     execute_from_command_line(sys.argv) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| django~=4.1 | ||||
| pyld~=2.0.3 | ||||
| pillow~=9.3.0 | ||||
| urlman~=2.0.1 | ||||
| django-crispy-forms~=1.14 | ||||
| cryptography~=38.0 | ||||
							
								
								
									
										227
									
								
								static/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								static/css/style.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,227 @@ | ||||
| /* Reset CSS */ | ||||
| 
 | ||||
| *, | ||||
| *::before, | ||||
| *::after { | ||||
|     box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| body, | ||||
| h1, | ||||
| h2, | ||||
| h3, | ||||
| h4, | ||||
| p, | ||||
| figure, | ||||
| blockquote, | ||||
| dl, | ||||
| dd, | ||||
| menu { | ||||
|     margin: 0; | ||||
| } | ||||
| 
 | ||||
| ul[role='list'], | ||||
| ol[role='list'] { | ||||
|     list-style: none; | ||||
| } | ||||
| 
 | ||||
| html:focus-within { | ||||
|     scroll-behavior: smooth; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|     min-height: 100vh; | ||||
|     text-rendering: optimizeSpeed; | ||||
|     line-height: 1.5; | ||||
|     font-family: sans-serif; | ||||
| } | ||||
| 
 | ||||
| a:not([class]) { | ||||
|     text-decoration-skip-ink: auto; | ||||
| } | ||||
| 
 | ||||
| img, | ||||
| picture { | ||||
|     max-width: 100%; | ||||
|     display: block; | ||||
| } | ||||
| 
 | ||||
| input, | ||||
| button, | ||||
| textarea, | ||||
| select { | ||||
|     font: inherit; | ||||
| } | ||||
| 
 | ||||
| @media (prefers-reduced-motion: reduce) { | ||||
|     html:focus-within { | ||||
|         scroll-behavior: auto; | ||||
|     } | ||||
| 
 | ||||
|     *, | ||||
|     *::before, | ||||
|     *::after { | ||||
|         animation-duration: 0.01ms !important; | ||||
|         animation-iteration-count: 1 !important; | ||||
|         transition-duration: 0.01ms !important; | ||||
|         scroll-behavior: auto !important; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /* Base template styling */ | ||||
| 
 | ||||
| :root { | ||||
|     --color-input-border: #000; | ||||
|     --color-input-border-active: #444b5d; | ||||
|     --color-button-main: #444b5d; | ||||
|     --color-button-main-hover: #515d7c; | ||||
|     --color-bg1: #191b22; | ||||
|     --color-bg2: #282c37; | ||||
|     --color-bg3: #444b5d; | ||||
|     --color-text-duller: #5f6983; | ||||
|     --color-text-dull: #99a; | ||||
|     --color-text-error: rgb(155, 111, 111); | ||||
|     --color-text-main: #DDDDDD; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|     background-color: var(--color-bg1); | ||||
|     color: white; | ||||
| } | ||||
| 
 | ||||
| header { | ||||
|     width: 750px; | ||||
|     margin: 0 auto; | ||||
|     display: flex; | ||||
|     padding: 0 0 20px 0; | ||||
| } | ||||
| 
 | ||||
| header h1 { | ||||
|     background: var(--color-fg2); | ||||
|     padding: 10px 7px 7px 7px; | ||||
|     font-size: 130%; | ||||
|     height: 2.2em; | ||||
|     color: var(--color-fg1); | ||||
| } | ||||
| 
 | ||||
| header a { | ||||
|     color: inherit; | ||||
|     text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| header menu { | ||||
|     flex-grow: 1; | ||||
|     display: flex; | ||||
|     list-style-type: none; | ||||
|     justify-content: flex-end; | ||||
| } | ||||
| 
 | ||||
| header menu li { | ||||
|     padding: 20px 10px 7px 10px; | ||||
|     color: #eee; | ||||
| } | ||||
| 
 | ||||
| main { | ||||
|     width: 750px; | ||||
|     margin: 20px auto; | ||||
| } | ||||
| 
 | ||||
| /* "Modal" boxes */ | ||||
| 
 | ||||
| .modal { | ||||
|     background: var(--color-bg2); | ||||
|     max-width: 500px; | ||||
|     margin: 0 auto; | ||||
|     box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); | ||||
|     border-radius: 5px; | ||||
| } | ||||
| 
 | ||||
| .modal h1 { | ||||
|     color: var(--color-fg1); | ||||
|     background: var(--color-bg3); | ||||
|     font-family: "Raleway"; | ||||
|     position: relative; | ||||
|     padding: 5px 8px 4px 10px; | ||||
|     font-size: 100%; | ||||
|     letter-spacing: 0.05em; | ||||
|     text-transform: uppercase; | ||||
|     border-radius: 5px 5px 0 0; | ||||
| } | ||||
| 
 | ||||
| .modal .option { | ||||
|     display: block; | ||||
|     padding: 20px 30px; | ||||
|     color: var(--color-text-main); | ||||
|     text-decoration: none; | ||||
|     border-left: 3px solid transparent; | ||||
| } | ||||
| 
 | ||||
| .modal a.option:hover { | ||||
|     border-left: 3px solid var(--color-text-dull); | ||||
| } | ||||
| 
 | ||||
| .modal .option.empty { | ||||
|     text-align: center; | ||||
|     color: var(--color-text-dull); | ||||
| } | ||||
| 
 | ||||
| .modal form { | ||||
|     padding: 10px 10px 1px 10px; | ||||
| } | ||||
| 
 | ||||
| /* Forms */ | ||||
| 
 | ||||
| form .control-group { | ||||
|     margin: 0 0 15px 0; | ||||
| } | ||||
| 
 | ||||
| form .asteriskField { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| form label { | ||||
|     text-transform: uppercase; | ||||
|     font-size: 110%; | ||||
|     color: var(--color-text-dull); | ||||
|     letter-spacing: 0.05em; | ||||
| } | ||||
| 
 | ||||
| form label.requiredField::after { | ||||
|     content: " (required)"; | ||||
|     font-size: 80%; | ||||
|     color: var(--color-text-duller); | ||||
| } | ||||
| 
 | ||||
| form .help-block { | ||||
|     color: var(--color-text-error); | ||||
|     padding: 4px 0 0 0; | ||||
| } | ||||
| 
 | ||||
| form input { | ||||
|     width: 100%; | ||||
|     padding: 4px 6px; | ||||
|     background: var(--color-bg1); | ||||
|     border: 1px solid var(--color-input-border); | ||||
|     border-radius: 3px; | ||||
|     color: var(--color-text-main); | ||||
| } | ||||
| 
 | ||||
| form input:focus { | ||||
|     outline: none; | ||||
|     border: 1px solid var(--color-input-border-active); | ||||
| } | ||||
| 
 | ||||
| form input[type=submit] { | ||||
|     width: 100%; | ||||
|     padding: 4px 6px; | ||||
|     margin: 0 0 10px; | ||||
|     background: var(--color-button-main); | ||||
|     border: 0; | ||||
|     border-radius: 3px; | ||||
|     color: var(--color-text-main); | ||||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| form input[type=submit]:hover { | ||||
|     background: var(--color-button-main-hover); | ||||
| } | ||||
							
								
								
									
										6
									
								
								static/fonts/font_awesome/all.min.css
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								static/fonts/font_awesome/all.min.css
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-brands-400.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-brands-400.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-brands-400.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-brands-400.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-regular-400.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-regular-400.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-regular-400.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-regular-400.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-solid-900.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-solid-900.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-solid-900.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-solid-900.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-v4compatibility.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-v4compatibility.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-v4compatibility.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/font_awesome/fa-v4compatibility.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Black.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Black.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-BlackItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-BlackItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Bold.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Bold.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-BoldItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-BoldItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraBold.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraBold.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraBoldItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraBoldItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraLight.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraLight.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraLightItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ExtraLightItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Italic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Italic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Light.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Light.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-LightItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-LightItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Medium.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Medium.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-MediumItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-MediumItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Regular.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Regular.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-SemiBold.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-SemiBold.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-SemiBoldItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-SemiBoldItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Thin.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-Thin.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ThinItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								static/fonts/raleway/Raleway-ThinItalic.woff2
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										20
									
								
								static/fonts/raleway/raleway.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								static/fonts/raleway/raleway.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| @font-face { | ||||
|     font-family: 'Raleway'; | ||||
|     src: url('Raleway-Bold.woff2'); | ||||
|     font-weight: bold; | ||||
|     font-style: normal; | ||||
| } | ||||
| 
 | ||||
| @font-face { | ||||
|     font-family: 'Raleway'; | ||||
|     src: url('Raleway-Regular.woff2'); | ||||
|     font-weight: normal; | ||||
|     font-style: normal; | ||||
| } | ||||
| 
 | ||||
| @font-face { | ||||
|     font-family: 'Raleway'; | ||||
|     src: url('Raleway-Light.woff2'); | ||||
|     font-weight: lighter; | ||||
|     font-style: normal; | ||||
| } | ||||
							
								
								
									
										0
									
								
								statuses/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								statuses/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										8
									
								
								statuses/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								statuses/admin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| from django.contrib import admin | ||||
| 
 | ||||
| from statuses.models import Status | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(Status) | ||||
| class StatusAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
							
								
								
									
										6
									
								
								statuses/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								statuses/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class StatusesConfig(AppConfig): | ||||
|     default_auto_field = "django.db.models.BigAutoField" | ||||
|     name = "statuses" | ||||
							
								
								
									
										56
									
								
								statuses/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								statuses/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| # Generated by Django 4.1.3 on 2022-11-05 19:43 | ||||
| 
 | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     initial = True | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ("users", "0001_initial"), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="Status", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.BigAutoField( | ||||
|                         auto_created=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         verbose_name="ID", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("local", models.BooleanField()), | ||||
|                 ("uri", models.CharField(blank=True, max_length=500, null=True)), | ||||
|                 ( | ||||
|                     "visibility", | ||||
|                     models.IntegerField( | ||||
|                         choices=[ | ||||
|                             (0, "Public"), | ||||
|                             (1, "Unlisted"), | ||||
|                             (2, "Followers"), | ||||
|                             (3, "Mentioned"), | ||||
|                         ], | ||||
|                         default=0, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("text", models.TextField()), | ||||
|                 ("created", models.DateTimeField(auto_now_add=True)), | ||||
|                 ("updated", models.DateTimeField(auto_now=True)), | ||||
|                 ("deleted", models.DateTimeField(blank=True, null=True)), | ||||
|                 ( | ||||
|                     "identity", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.PROTECT, | ||||
|                         related_name="statuses", | ||||
|                         to="users.identity", | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								statuses/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								statuses/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										1
									
								
								statuses/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								statuses/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| from .status import Status  # noqa | ||||
							
								
								
									
										35
									
								
								statuses/models/status.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								statuses/models/status.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| from django.db import models | ||||
| 
 | ||||
| 
 | ||||
| class Status(models.Model): | ||||
|     class StatusVisibility(models.IntegerChoices): | ||||
|         public = 0 | ||||
|         unlisted = 1 | ||||
|         followers = 2 | ||||
|         mentioned = 3 | ||||
| 
 | ||||
|     identity = models.ForeignKey( | ||||
|         "users.Identity", | ||||
|         on_delete=models.PROTECT, | ||||
|         related_name="statuses", | ||||
|     ) | ||||
| 
 | ||||
|     local = models.BooleanField() | ||||
|     uri = models.CharField(max_length=500, blank=True, null=True) | ||||
|     visibility = models.IntegerField( | ||||
|         choices=StatusVisibility.choices, | ||||
|         default=StatusVisibility.public, | ||||
|     ) | ||||
|     text = models.TextField() | ||||
| 
 | ||||
|     created = models.DateTimeField(auto_now_add=True) | ||||
|     updated = models.DateTimeField(auto_now=True) | ||||
|     deleted = models.DateTimeField(null=True, blank=True) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def create_local(cls, identity, text: str): | ||||
|         return cls.objects.create( | ||||
|             identity=identity, | ||||
|             text=text, | ||||
|             local=True, | ||||
|         ) | ||||
							
								
								
									
										0
									
								
								statuses/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								statuses/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										35
									
								
								statuses/views/home.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								statuses/views/home.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| from django import forms | ||||
| from django.shortcuts import redirect | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.views.generic import FormView | ||||
| 
 | ||||
| from core.forms import FormHelper | ||||
| from statuses.models import Status | ||||
| from users.decorators import identity_required | ||||
| 
 | ||||
| 
 | ||||
| @method_decorator(identity_required, name="dispatch") | ||||
| class Home(FormView): | ||||
| 
 | ||||
|     template_name = "statuses/home.html" | ||||
| 
 | ||||
|     class form_class(forms.Form): | ||||
|         text = forms.CharField() | ||||
| 
 | ||||
|         helper = FormHelper(submit_text="Post") | ||||
| 
 | ||||
|     def get_context_data(self): | ||||
|         context = super().get_context_data() | ||||
|         context.update( | ||||
|             { | ||||
|                 "statuses": self.request.identity.statuses.all()[:100], | ||||
|             } | ||||
|         ) | ||||
|         return context | ||||
| 
 | ||||
|     def form_valid(self, form): | ||||
|         Status.create_local( | ||||
|             identity=self.request.identity, | ||||
|             text=form.cleaned_data["text"], | ||||
|         ) | ||||
|         return redirect(".") | ||||
							
								
								
									
										0
									
								
								takahe/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								takahe/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										16
									
								
								takahe/asgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								takahe/asgi.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| """ | ||||
| ASGI config for takahe project. | ||||
| 
 | ||||
| It exposes the ASGI callable as a module-level variable named ``application``. | ||||
| 
 | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| 
 | ||||
| from django.core.asgi import get_asgi_application | ||||
| 
 | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "takahe.settings") | ||||
| 
 | ||||
| application = get_asgi_application() | ||||
							
								
								
									
										115
									
								
								takahe/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								takahe/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | ||||
| from pathlib import Path | ||||
| 
 | ||||
| # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||||
| BASE_DIR = Path(__file__).resolve().parent.parent | ||||
| 
 | ||||
| # SECURITY WARNING: keep the secret key used in production secret! | ||||
| SECRET_KEY = "insecure_secret" | ||||
| 
 | ||||
| # SECURITY WARNING: don't run with debug turned on in production! | ||||
| DEBUG = True | ||||
| 
 | ||||
| ALLOWED_HOSTS = ["*"] | ||||
| 
 | ||||
| 
 | ||||
| # Application definition | ||||
| 
 | ||||
| INSTALLED_APPS = [ | ||||
|     "django.contrib.admin", | ||||
|     "django.contrib.auth", | ||||
|     "django.contrib.contenttypes", | ||||
|     "django.contrib.sessions", | ||||
|     "django.contrib.messages", | ||||
|     "django.contrib.staticfiles", | ||||
|     "crispy_forms", | ||||
|     "core", | ||||
|     "statuses", | ||||
|     "users", | ||||
| ] | ||||
| 
 | ||||
| MIDDLEWARE = [ | ||||
|     "django.middleware.security.SecurityMiddleware", | ||||
|     "django.contrib.sessions.middleware.SessionMiddleware", | ||||
|     "django.middleware.common.CommonMiddleware", | ||||
|     "django.middleware.csrf.CsrfViewMiddleware", | ||||
|     "django.contrib.auth.middleware.AuthenticationMiddleware", | ||||
|     "django.contrib.messages.middleware.MessageMiddleware", | ||||
|     "django.middleware.clickjacking.XFrameOptionsMiddleware", | ||||
| ] | ||||
| 
 | ||||
| ROOT_URLCONF = "takahe.urls" | ||||
| 
 | ||||
| TEMPLATES = [ | ||||
|     { | ||||
|         "BACKEND": "django.template.backends.django.DjangoTemplates", | ||||
|         "DIRS": [BASE_DIR / "templates"], | ||||
|         "APP_DIRS": True, | ||||
|         "OPTIONS": { | ||||
|             "context_processors": [ | ||||
|                 "django.template.context_processors.debug", | ||||
|                 "django.template.context_processors.request", | ||||
|                 "django.contrib.auth.context_processors.auth", | ||||
|                 "django.contrib.messages.context_processors.messages", | ||||
|                 "core.context.config_context", | ||||
|             ], | ||||
|         }, | ||||
|     }, | ||||
| ] | ||||
| 
 | ||||
| WSGI_APPLICATION = "takahe.wsgi.application" | ||||
| 
 | ||||
| DATABASES = { | ||||
|     "default": { | ||||
|         "ENGINE": "django.db.backends.sqlite3", | ||||
|         "NAME": BASE_DIR / "db.sqlite3", | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| AUTH_PASSWORD_VALIDATORS = [ | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", | ||||
|     }, | ||||
|     { | ||||
|         "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", | ||||
|     }, | ||||
| ] | ||||
| 
 | ||||
| LANGUAGE_CODE = "en-us" | ||||
| 
 | ||||
| TIME_ZONE = "UTC" | ||||
| 
 | ||||
| USE_I18N = True | ||||
| 
 | ||||
| USE_TZ = True | ||||
| 
 | ||||
| STATIC_URL = "static/" | ||||
| 
 | ||||
| DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" | ||||
| 
 | ||||
| AUTH_USER_MODEL = "users.User" | ||||
| 
 | ||||
| LOGIN_URL = "/auth/login/" | ||||
| LOGOUT_URL = "/auth/logout/" | ||||
| LOGIN_REDIRECT_URL = "/" | ||||
| LOGOUT_REDIRECT_URL = "/" | ||||
| 
 | ||||
| STATICFILES_FINDERS = [ | ||||
|     "django.contrib.staticfiles.finders.FileSystemFinder", | ||||
|     "django.contrib.staticfiles.finders.AppDirectoriesFinder", | ||||
| ] | ||||
| 
 | ||||
| STATICFILES_DIRS = [ | ||||
|     BASE_DIR / "static", | ||||
| ] | ||||
| 
 | ||||
| CRISPY_FAIL_SILENTLY = not DEBUG | ||||
| 
 | ||||
| SITE_NAME = "takahē" | ||||
| DEFAULT_DOMAIN = "feditest.aeracode.org" | ||||
| ALLOWED_DOMAINS = ["feditest.aeracode.org"] | ||||
							
								
								
									
										22
									
								
								takahe/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								takahe/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| from django.contrib import admin | ||||
| from django.urls import path | ||||
| 
 | ||||
| from core import views as core | ||||
| from users.views import auth, identity | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     path("", core.homepage), | ||||
|     # Authentication | ||||
|     path("auth/login/", auth.Login.as_view()), | ||||
|     path("auth/logout/", auth.Logout.as_view()), | ||||
|     # Identity views | ||||
|     path("@<handle>/", identity.ViewIdentity.as_view()), | ||||
|     path("@<handle>/actor/", identity.Actor.as_view()), | ||||
|     # Identity selection | ||||
|     path("identity/select/", identity.SelectIdentity.as_view()), | ||||
|     path("identity/create/", identity.CreateIdentity.as_view()), | ||||
|     # Well-known endpoints | ||||
|     path(".well-known/webfinger/", identity.Webfinger.as_view()), | ||||
|     # Django admin | ||||
|     path("djadmin/", admin.site.urls), | ||||
| ] | ||||
							
								
								
									
										16
									
								
								takahe/wsgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								takahe/wsgi.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| """ | ||||
| WSGI config for takahe project. | ||||
| 
 | ||||
| It exposes the WSGI callable as a module-level variable named ``application``. | ||||
| 
 | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| 
 | ||||
| from django.core.wsgi import get_wsgi_application | ||||
| 
 | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "takahe.settings") | ||||
| 
 | ||||
| application = get_wsgi_application() | ||||
							
								
								
									
										6
									
								
								templates/404.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								templates/404.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block content %} | ||||
|     <h1>Page Not Found</h1> | ||||
|     <p>Sorry about that.</p> | ||||
| {% endblock %} | ||||
							
								
								
									
										11
									
								
								templates/auth/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								templates/auth/login.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| {% extends "base.html" %} | ||||
| {% load crispy_forms_tags %} | ||||
| 
 | ||||
| {% block title %}Login{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
|     <section class="modal identities"> | ||||
|         <h1>Login</h1> | ||||
|         {% crispy form form.helper %} | ||||
|     </section> | ||||
| {% endblock %} | ||||
							
								
								
									
										34
									
								
								templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								templates/base.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>{% block title %}{% endblock %} - {{ config.site_name }}</title> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     {% load static %} | ||||
|     <link rel="stylesheet" href="{% static "css/style.css" %}" type="text/css" media="screen" /> | ||||
|     <link rel="stylesheet" href="{% static "fonts/raleway/raleway.css" %}" type="text/css" /> | ||||
|     <link rel="stylesheet" href="{% static "fonts/font_awesome/all.min.css" %}" type="text/css" /> | ||||
|     {% block extra_head %}{% endblock %} | ||||
| </head> | ||||
| <body class="{% block body_class %}{% endblock %}"> | ||||
| 
 | ||||
|     <header> | ||||
|         <h1><a href="/">{{ config.site_name }}</a></h1> | ||||
|         <menu> | ||||
|             <li> | ||||
|                 {% if user.is_authenticated %} | ||||
|                     {{ user.email }} | ||||
|                 {% else %} | ||||
|                     <a href="/auth/login/">Login</a> | ||||
|                 {% endif %} | ||||
|             </li> | ||||
|         </menu> | ||||
|     </header> | ||||
| 
 | ||||
|     <main> | ||||
|         {% block content %} | ||||
|         {% endblock %} | ||||
|     </main> | ||||
| 
 | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										12
									
								
								templates/identity/create.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								templates/identity/create.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% load crispy_forms_tags %} | ||||
| 
 | ||||
| {% block title %}Create Identity{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
|     <section class="modal identities"> | ||||
|         <h1>Create Identity</h1> | ||||
|         {% crispy form form.helper %} | ||||
|     </section> | ||||
| {% endblock %} | ||||
							
								
								
									
										17
									
								
								templates/identity/select.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								templates/identity/select.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block title %}Select Identity{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
|     <section class="modal identities"> | ||||
|         <h1>Select Identity</h1> | ||||
|         {% for identity in identities %} | ||||
|             <a class="option" href="{{ identity.urls.activate }}">{{ identity }}</a> | ||||
|         {% empty %} | ||||
|             <p class="option empty">You have no identities.</p> | ||||
|         {% endfor %} | ||||
|         <a href="/identity/create/" class="option new"> | ||||
|             <i class="fa-solid fa-plus"></i> Create a new identity | ||||
|         </a> | ||||
|     </section> | ||||
| {% endblock %} | ||||
							
								
								
									
										13
									
								
								templates/identity/view.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								templates/identity/view.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block title %}{{ identity }}{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
|     <h1>{{ identity }} <small>{{ identity.handle }}</small></h1> | ||||
| 
 | ||||
|     {% for status in statuses %} | ||||
|         {% include "statuses/_status.html" %} | ||||
|     {% empty %} | ||||
|         No statuses yet. | ||||
|     {% endfor %} | ||||
| {% endblock %} | ||||
							
								
								
									
										9
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block title %}Welcome{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
|     {% for identity in identities %} | ||||
|         <a href="{{ identity.urls.view }}">{{ identity }}</a> | ||||
|     {% endfor %} | ||||
| {% endblock %} | ||||
							
								
								
									
										10
									
								
								templates/statuses/_status.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								templates/statuses/_status.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <div class="status"> | ||||
|     <h3 class="author"> | ||||
|         <a href="{{ status.identity.urls.view }}"> | ||||
|             {{ status.identity }} | ||||
|             <small>{{ status.identity.short_handle }}</small> | ||||
|         </a> | ||||
|     </h3> | ||||
|     <time>{{ status.created | timesince }} ago</time> | ||||
|     {{ status.text | linebreaks }} | ||||
| </div> | ||||
							
								
								
									
										15
									
								
								templates/statuses/home.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								templates/statuses/home.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| {% extends "base.html" %} | ||||
| {% load crispy_forms_tags %} | ||||
| 
 | ||||
| {% block title %}Home{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
|     {% crispy form form.helper %} | ||||
| 
 | ||||
|     {% for status in statuses %} | ||||
|         {% include "statuses/_status.html" %} | ||||
|     {% empty %} | ||||
|         No statuses yet. | ||||
|     {% endfor %} | ||||
| {% endblock %} | ||||
							
								
								
									
										0
									
								
								users/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								users/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										18
									
								
								users/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								users/admin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| from django.contrib import admin | ||||
| 
 | ||||
| from users.models import Identity, User, UserEvent | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(User) | ||||
| class UserAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(UserEvent) | ||||
| class UserEventAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(Identity) | ||||
| class IdentityAdmin(admin.ModelAdmin): | ||||
|     pass | ||||
							
								
								
									
										6
									
								
								users/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								users/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class UsersConfig(AppConfig): | ||||
|     default_auto_field = "django.db.models.BigAutoField" | ||||
|     name = "users" | ||||
							
								
								
									
										39
									
								
								users/decorators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								users/decorators.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| from functools import wraps | ||||
| 
 | ||||
| from django.contrib.auth.views import redirect_to_login | ||||
| from django.http import HttpResponseRedirect | ||||
| 
 | ||||
| from users.models import Identity | ||||
| 
 | ||||
| 
 | ||||
| def identity_required(function): | ||||
|     """ | ||||
|     Decorator for views that ensures an active identity is selected. | ||||
|     """ | ||||
| 
 | ||||
|     @wraps(function) | ||||
|     def inner(request, *args, **kwargs): | ||||
|         # They do have to be logged in | ||||
|         if not request.user.is_authenticated: | ||||
|             return redirect_to_login(next=request.get_full_path()) | ||||
|         # Try to retrieve their active identity | ||||
|         identity_id = request.session.get("identity_id") | ||||
|         if not identity_id: | ||||
|             identity = None | ||||
|         else: | ||||
|             try: | ||||
|                 identity = Identity.objects.get(id=identity_id) | ||||
|             except Identity.DoesNotExist: | ||||
|                 identity = None | ||||
|         # If there's no active one, try to auto-select one | ||||
|         if identity is None: | ||||
|             possible_identities = list(request.user.identities.all()) | ||||
|             if len(possible_identities) != 1: | ||||
|                 # OK, send them to the identity selection page to select/create one | ||||
|                 return HttpResponseRedirect("/identity/select/") | ||||
|             identity = possible_identities[0] | ||||
|         request.identity = identity | ||||
|         request.session["identity_id"] = identity.pk | ||||
|         return function(request, *args, **kwargs) | ||||
| 
 | ||||
|     return inner | ||||
							
								
								
									
										134
									
								
								users/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								users/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,134 @@ | ||||
| # Generated by Django 4.1.3 on 2022-11-05 19:15 | ||||
| 
 | ||||
| import functools | ||||
| 
 | ||||
| import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| import users.models.identity | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     initial = True | ||||
| 
 | ||||
|     dependencies = [] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="User", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.BigAutoField( | ||||
|                         auto_created=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         verbose_name="ID", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("password", models.CharField(max_length=128, verbose_name="password")), | ||||
|                 ( | ||||
|                     "last_login", | ||||
|                     models.DateTimeField( | ||||
|                         blank=True, null=True, verbose_name="last login" | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("email", models.EmailField(max_length=254, unique=True)), | ||||
|                 ("admin", models.BooleanField(default=False)), | ||||
|                 ("moderator", models.BooleanField(default=False)), | ||||
|                 ("banned", models.BooleanField(default=False)), | ||||
|                 ("deleted", models.BooleanField(default=False)), | ||||
|                 ("created", models.DateTimeField(auto_now_add=True)), | ||||
|                 ("updated", models.DateTimeField(auto_now=True)), | ||||
|             ], | ||||
|             options={ | ||||
|                 "abstract": False, | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="UserEvent", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.BigAutoField( | ||||
|                         auto_created=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         verbose_name="ID", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("date", models.DateTimeField(auto_now_add=True)), | ||||
|                 ( | ||||
|                     "type", | ||||
|                     models.CharField( | ||||
|                         choices=[ | ||||
|                             ("created", "Created"), | ||||
|                             ("reset_password", "Reset Password"), | ||||
|                             ("banned", "Banned"), | ||||
|                         ], | ||||
|                         max_length=100, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("data", models.JSONField(blank=True, null=True)), | ||||
|                 ( | ||||
|                     "user", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         related_name="events", | ||||
|                         to=settings.AUTH_USER_MODEL, | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="Identity", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.BigAutoField( | ||||
|                         auto_created=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         verbose_name="ID", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("handle", models.CharField(max_length=500, unique=True)), | ||||
|                 ("name", models.CharField(blank=True, max_length=500, null=True)), | ||||
|                 ("bio", models.TextField(blank=True, null=True)), | ||||
|                 ( | ||||
|                     "profile_image", | ||||
|                     models.ImageField( | ||||
|                         upload_to=functools.partial( | ||||
|                             users.models.identity.upload_namer, | ||||
|                             *("profile_images",), | ||||
|                             **{}, | ||||
|                         ) | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "background_image", | ||||
|                     models.ImageField( | ||||
|                         upload_to=functools.partial( | ||||
|                             users.models.identity.upload_namer, | ||||
|                             *("background_images",), | ||||
|                             **{}, | ||||
|                         ) | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("local", models.BooleanField()), | ||||
|                 ("private_key", models.BinaryField(blank=True, null=True)), | ||||
|                 ("public_key", models.BinaryField(blank=True, null=True)), | ||||
|                 ("created", models.DateTimeField(auto_now_add=True)), | ||||
|                 ("updated", models.DateTimeField(auto_now=True)), | ||||
|                 ("deleted", models.DateTimeField(blank=True, null=True)), | ||||
|                 ( | ||||
|                     "users", | ||||
|                     models.ManyToManyField( | ||||
|                         related_name="identities", to=settings.AUTH_USER_MODEL | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								users/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								users/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								users/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								users/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| from .identity import Identity  # noqa | ||||
| from .user import User  # noqa | ||||
| from .user_event import UserEvent  # noqa | ||||
							
								
								
									
										79
									
								
								users/models/identity.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								users/models/identity.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | ||||
| import base64 | ||||
| import uuid | ||||
| from functools import partial | ||||
| 
 | ||||
| import urlman | ||||
| from cryptography.hazmat.primitives import serialization | ||||
| from cryptography.hazmat.primitives.asymmetric import rsa | ||||
| from django.conf import settings | ||||
| from django.db import models | ||||
| from django.utils import timezone | ||||
| 
 | ||||
| 
 | ||||
| def upload_namer(prefix, instance, filename): | ||||
|     """ | ||||
|     Names uploaded images etc. | ||||
|     """ | ||||
|     now = timezone.now() | ||||
|     filename = base64.b32encode(uuid.uuid4().bytes).decode("ascii") | ||||
|     return f"{prefix}/{now.year}/{now.month}/{now.day}/{filename}" | ||||
| 
 | ||||
| 
 | ||||
| class Identity(models.Model): | ||||
|     """ | ||||
|     Represents both local and remote Fediverse identities (actors) | ||||
|     """ | ||||
| 
 | ||||
|     # The handle includes the domain! | ||||
|     handle = models.CharField(max_length=500, unique=True) | ||||
|     name = models.CharField(max_length=500, blank=True, null=True) | ||||
|     bio = models.TextField(blank=True, null=True) | ||||
| 
 | ||||
|     profile_image = models.ImageField(upload_to=partial(upload_namer, "profile_images")) | ||||
|     background_image = models.ImageField( | ||||
|         upload_to=partial(upload_namer, "background_images") | ||||
|     ) | ||||
| 
 | ||||
|     local = models.BooleanField() | ||||
|     users = models.ManyToManyField("users.User", related_name="identities") | ||||
|     private_key = models.TextField(null=True, blank=True) | ||||
|     public_key = models.TextField(null=True, blank=True) | ||||
| 
 | ||||
|     created = models.DateTimeField(auto_now_add=True) | ||||
|     updated = models.DateTimeField(auto_now=True) | ||||
|     deleted = models.DateTimeField(null=True, blank=True) | ||||
| 
 | ||||
|     @property | ||||
|     def short_handle(self): | ||||
|         if self.handle.endswith("@" + settings.DEFAULT_DOMAIN): | ||||
|             return self.handle.split("@", 1)[0] | ||||
|         return self.handle | ||||
| 
 | ||||
|     @property | ||||
|     def domain(self): | ||||
|         return self.handle.split("@", 1)[1] | ||||
| 
 | ||||
|     def generate_keypair(self): | ||||
|         private_key = rsa.generate_private_key( | ||||
|             public_exponent=65537, | ||||
|             key_size=2048, | ||||
|         ) | ||||
|         self.private_key = private_key.private_bytes( | ||||
|             encoding=serialization.Encoding.PEM, | ||||
|             format=serialization.PrivateFormat.PKCS8, | ||||
|             encryption_algorithm=serialization.NoEncryption(), | ||||
|         ) | ||||
|         self.public_key = private_key.public_key().public_bytes( | ||||
|             encoding=serialization.Encoding.PEM, | ||||
|             format=serialization.PublicFormat.SubjectPublicKeyInfo, | ||||
|         ) | ||||
|         self.save() | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name or self.handle | ||||
| 
 | ||||
|     class urls(urlman.Urls): | ||||
|         view = "/@{self.short_handle}/" | ||||
|         actor = "{view}actor/" | ||||
|         inbox = "{actor}inbox/" | ||||
|         activate = "{view}activate/" | ||||
							
								
								
									
										58
									
								
								users/models/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								users/models/user.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| from typing import List | ||||
| 
 | ||||
| from django.contrib.auth.models import AbstractBaseUser, BaseUserManager | ||||
| from django.db import models | ||||
| 
 | ||||
| 
 | ||||
| class UserManager(BaseUserManager): | ||||
|     """ | ||||
|     Custom user manager that understands emails | ||||
|     """ | ||||
| 
 | ||||
|     def create_user(self, email, password=None): | ||||
|         user = self.create(email=email) | ||||
|         if password: | ||||
|             user.set_password(password) | ||||
|             user.save() | ||||
|         return user | ||||
| 
 | ||||
|     def create_superuser(self, email, password=None): | ||||
|         user = self.create(email=email, admin=True) | ||||
|         if password: | ||||
|             user.set_password(password) | ||||
|             user.save() | ||||
|         return user | ||||
| 
 | ||||
| 
 | ||||
| class User(AbstractBaseUser): | ||||
|     """ | ||||
|     Custom user model that only needs an email | ||||
|     """ | ||||
| 
 | ||||
|     email = models.EmailField(unique=True) | ||||
| 
 | ||||
|     admin = models.BooleanField(default=False) | ||||
|     moderator = models.BooleanField(default=False) | ||||
|     banned = models.BooleanField(default=False) | ||||
|     deleted = models.BooleanField(default=False) | ||||
| 
 | ||||
|     created = models.DateTimeField(auto_now_add=True) | ||||
|     updated = models.DateTimeField(auto_now=True) | ||||
| 
 | ||||
|     USERNAME_FIELD = "email" | ||||
|     EMAIL_FIELD = "email" | ||||
|     REQUIRED_FIELDS: List[str] = [] | ||||
| 
 | ||||
|     objects = UserManager() | ||||
| 
 | ||||
|     @property | ||||
|     def is_active(self): | ||||
|         return not (self.deleted or self.banned) | ||||
| 
 | ||||
|     @property | ||||
|     def is_superuser(self): | ||||
|         return self.admin | ||||
| 
 | ||||
|     @property | ||||
|     def is_staff(self): | ||||
|         return self.admin | ||||
							
								
								
									
										22
									
								
								users/models/user_event.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								users/models/user_event.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| from django.db import models | ||||
| 
 | ||||
| 
 | ||||
| class UserEvent(models.Model): | ||||
|     """ | ||||
|     Tracks major events that happen to users | ||||
|     """ | ||||
| 
 | ||||
|     class EventType(models.TextChoices): | ||||
|         created = "created" | ||||
|         reset_password = "reset_password" | ||||
|         banned = "banned" | ||||
| 
 | ||||
|     user = models.ForeignKey( | ||||
|         "users.User", | ||||
|         on_delete=models.CASCADE, | ||||
|         related_name="events", | ||||
|     ) | ||||
| 
 | ||||
|     date = models.DateTimeField(auto_now_add=True) | ||||
|     type = models.CharField(max_length=100, choices=EventType.choices) | ||||
|     data = models.JSONField(blank=True, null=True) | ||||
							
								
								
									
										18
									
								
								users/shortcuts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								users/shortcuts.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| from django.conf import settings | ||||
| from django.shortcuts import get_object_or_404 | ||||
| 
 | ||||
| from users.models import Identity | ||||
| 
 | ||||
| 
 | ||||
| def by_handle_or_404(request, handle, local=True): | ||||
|     """ | ||||
|     Retrieves an Identity by its long or short handle. | ||||
|     Domain-sensitive, so it will understand short handles on alternate domains. | ||||
|     """ | ||||
|     # TODO: Domain sensitivity | ||||
|     if "@" not in handle: | ||||
|         handle += "@" + settings.DEFAULT_DOMAIN | ||||
|     if local: | ||||
|         return get_object_or_404(Identity.objects.filter(local=True), handle=handle) | ||||
|     else: | ||||
|         return get_object_or_404(Identity, handle=handle) | ||||
							
								
								
									
										1
									
								
								users/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								users/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| from .auth import *  # noqa | ||||
							
								
								
									
										15
									
								
								users/views/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								users/views/auth.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| from django.contrib.auth.forms import AuthenticationForm | ||||
| from django.contrib.auth.views import LoginView, LogoutView | ||||
| 
 | ||||
| from core.forms import FormHelper | ||||
| 
 | ||||
| 
 | ||||
| class Login(LoginView): | ||||
|     class form_class(AuthenticationForm): | ||||
|         helper = FormHelper(submit_text="Login") | ||||
| 
 | ||||
|     template_name = "auth/login.html" | ||||
| 
 | ||||
| 
 | ||||
| class Logout(LogoutView): | ||||
|     pass | ||||
							
								
								
									
										132
									
								
								users/views/identity.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								users/views/identity.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.decorators import login_required | ||||
| from django.http import Http404, JsonResponse | ||||
| from django.shortcuts import redirect | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.views.generic import FormView, TemplateView, View | ||||
| 
 | ||||
| from core.forms import FormHelper | ||||
| from users.models import Identity | ||||
| from users.shortcuts import by_handle_or_404 | ||||
| 
 | ||||
| 
 | ||||
| class ViewIdentity(TemplateView): | ||||
| 
 | ||||
|     template_name = "identity/view.html" | ||||
| 
 | ||||
|     def get_context_data(self, handle): | ||||
|         identity = by_handle_or_404(self.request, handle, local=False) | ||||
|         statuses = identity.statuses.all()[:100] | ||||
|         return { | ||||
|             "identity": identity, | ||||
|             "statuses": statuses, | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| @method_decorator(login_required, name="dispatch") | ||||
| class SelectIdentity(TemplateView): | ||||
| 
 | ||||
|     template_name = "identity/select.html" | ||||
| 
 | ||||
|     def get_context_data(self): | ||||
|         return { | ||||
|             "identities": Identity.objects.filter(users__pk=self.request.user.pk), | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| @method_decorator(login_required, name="dispatch") | ||||
| class CreateIdentity(FormView): | ||||
| 
 | ||||
|     template_name = "identity/create.html" | ||||
| 
 | ||||
|     class form_class(forms.Form): | ||||
|         handle = forms.CharField() | ||||
|         name = forms.CharField() | ||||
| 
 | ||||
|         helper = FormHelper(submit_text="Create") | ||||
| 
 | ||||
|         def clean_handle(self): | ||||
|             # Remove any leading @ | ||||
|             value = self.cleaned_data["handle"].lstrip("@") | ||||
|             # Don't allow custom domains here quite yet | ||||
|             if "@" in value: | ||||
|                 raise forms.ValidationError( | ||||
|                     "You are not allowed an @ sign in your handle" | ||||
|                 ) | ||||
|             # Ensure there is a domain on the end | ||||
|             if "@" not in value: | ||||
|                 value += "@" + settings.DEFAULT_DOMAIN | ||||
|             # Check for existing users | ||||
|             if Identity.objects.filter(handle=value).exists(): | ||||
|                 raise forms.ValidationError("This handle is already taken") | ||||
|             return value | ||||
| 
 | ||||
|     def form_valid(self, form): | ||||
|         new_identity = Identity.objects.create( | ||||
|             handle=form.cleaned_data["handle"], | ||||
|             name=form.cleaned_data["name"], | ||||
|             local=True, | ||||
|         ) | ||||
|         new_identity.users.add(self.request.user) | ||||
|         new_identity.generate_keypair() | ||||
|         return redirect(new_identity.urls.view) | ||||
| 
 | ||||
| 
 | ||||
| class Actor(View): | ||||
|     """ | ||||
|     Returns the AP Actor object | ||||
|     """ | ||||
| 
 | ||||
|     def get(self, request, handle): | ||||
|         identity = by_handle_or_404(self.request, handle) | ||||
|         return JsonResponse( | ||||
|             { | ||||
|                 "@context": [ | ||||
|                     "https://www.w3.org/ns/activitystreams", | ||||
|                     "https://w3id.org/security/v1", | ||||
|                 ], | ||||
|                 "id": f"https://{settings.DEFAULT_DOMAIN}{identity.urls.actor}", | ||||
|                 "type": "Person", | ||||
|                 "preferredUsername": "alice", | ||||
|                 "inbox": f"https://{settings.DEFAULT_DOMAIN}{identity.urls.inbox}", | ||||
|                 "publicKey": { | ||||
|                     "id": f"https://{settings.DEFAULT_DOMAIN}{identity.urls.actor}#main-key", | ||||
|                     "owner": f"https://{settings.DEFAULT_DOMAIN}{identity.urls.actor}", | ||||
|                     "publicKeyPem": identity.public_key, | ||||
|                 }, | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class Webfinger(View): | ||||
|     """ | ||||
|     Services webfinger requests | ||||
|     """ | ||||
| 
 | ||||
|     def get(self, request): | ||||
|         resource = request.GET.get("resource") | ||||
|         if not resource.startswith("acct:"): | ||||
|             raise Http404("Not an account resource") | ||||
|         handle = resource[5:] | ||||
|         identity = by_handle_or_404(request, handle) | ||||
|         return JsonResponse( | ||||
|             { | ||||
|                 "subject": f"acct:{identity.handle}", | ||||
|                 "aliases": [ | ||||
|                     f"https://{settings.DEFAULT_DOMAIN}/@{identity.short_handle}", | ||||
|                 ], | ||||
|                 "links": [ | ||||
|                     { | ||||
|                         "rel": "http://webfinger.net/rel/profile-page", | ||||
|                         "type": "text/html", | ||||
|                         "href": f"https://{settings.DEFAULT_DOMAIN}{identity.urls.view}", | ||||
|                     }, | ||||
|                     { | ||||
|                         "rel": "self", | ||||
|                         "type": "application/activity+json", | ||||
|                         "href": f"https://{settings.DEFAULT_DOMAIN}{identity.urls.actor}", | ||||
|                     }, | ||||
|                 ], | ||||
|             } | ||||
|         ) | ||||
		Reference in New Issue
	
	Block a user
	 Andrew Godwin
						Andrew Godwin