├── .gitignore
├── .gitlab-ci.yml
├── COPYING
├── NEWS
├── README.md
├── build-aux
└── meson
│ └── postinstall.py
├── data
├── icons
│ ├── hicolor
│ │ ├── scalable
│ │ │ └── apps
│ │ │ │ └── org.gnome.gitlab.ranchester.Mirdorph.svg
│ │ └── symbolic
│ │ │ └── apps
│ │ │ └── org.gnome.gitlab.ranchester.Mirdorph-symbolic.svg
│ ├── meson.build
│ ├── org.gnome.gitlab.ranchester.Mirdorph.Source.svg
│ └── scalable
│ │ └── status
│ │ ├── chat-communication.svg
│ │ ├── gear-symbolic.svg
│ │ ├── org.gnome.gitlab.ranchester.Mirdorph.DiscussionsLogo.svg
│ │ ├── org.gnome.gitlab.ranchester.Mirdorph.Guilds-symbolic.svg
│ │ ├── paper-plane-symbolic.svg
│ │ ├── smile-symbolic.svg
│ │ └── view-sidebar-symbolic.svg
├── meson.build
├── mirdorph.gresource.xml
├── org.gnome.gitlab.ranchester.Mirdorph.appdata.xml.in
├── org.gnome.gitlab.ranchester.Mirdorph.desktop.in
├── org.gnome.gitlab.ranchester.Mirdorph.gschema.xml
├── style.css
├── tos.txt
└── ui
│ ├── about_dialog.ui.in
│ ├── channel_inner_window.ui
│ ├── channel_list_entry.ui
│ ├── channel_properties_window.ui
│ ├── channel_sidebar.ui
│ ├── context_error_dialog.ui
│ ├── extension_row.ui
│ ├── generic_attachment.ui
│ ├── image_viewer.ui
│ ├── link_preview.ui
│ ├── login_window.ui
│ ├── main_window.ui
│ ├── meson.build
│ ├── message.ui
│ ├── message_entry_bar.ui
│ ├── message_entry_bar_attachment.ui
│ ├── message_view.ui
│ ├── settings_window.ui
│ ├── tos_notice.ui
│ └── typing_indicator.ui
├── doc
├── STANDARD.md
├── asset
│ ├── mirdorph-channel-properties.png
│ ├── mirdorph-guild-search.png
│ ├── mirdorph-image-viewer.png
│ ├── mirdorph-loading-screen.png
│ ├── mirdorph-login-gui.png
│ ├── mirdorph-login-password.png
│ ├── mirdorph-login-token.png
│ ├── mirdorph-login.png
│ ├── mirdorph-mobile-with-sidebar.png
│ ├── mirdorph-mobile.png
│ ├── mirdorph-popped-out.png
│ ├── mirdorph-unselected-main-win.png
│ └── mirdorph-with-channel.png
├── discord-maintainability.txt
├── on_message_in-selfbot-and-other-issues-info-attempt
├── on_message_self_bots_broken.md
├── plugin.md
└── slow-window-focusing.txt
├── meson.build
├── mirdorph
├── __init__.py
├── attachment.py
├── channel_inner_window.py
├── channel_properties_window.py
├── channel_sidebar.py
├── confman.py
├── disc_cogs
│ ├── event_listening.py
│ └── meson.build
├── event_manager.py
├── event_receiver.py
├── image_viewer.py
├── link_preview.py
├── login_window.py
├── main.py
├── main_window.py
├── meson.build
├── message.py
├── message_entry_bar.py
├── message_parsing.py
├── message_view.py
├── mirdorph.in
├── plugin.py
├── plugins
│ ├── helloworld
│ │ ├── configuration.ui
│ │ ├── helloworld.gresource.xml
│ │ ├── helloworld.plugin
│ │ ├── helloworld.py
│ │ ├── meson.build
│ │ └── welcome_message.txt
│ ├── login_method_graphical
│ │ ├── discord_web_grabber.py
│ │ ├── get_token.js
│ │ ├── login_method_graphical.plugin
│ │ ├── login_method_graphical.py
│ │ └── meson.build
│ ├── login_method_manual
│ │ ├── login_method_manual.gresource.xml
│ │ ├── login_method_manual.plugin
│ │ ├── login_method_manual.py
│ │ ├── manual_login_page.ui
│ │ └── meson.build
│ ├── login_method_password
│ │ ├── login_method_password.gresource.xml
│ │ ├── login_method_password.plugin
│ │ ├── login_method_password.py
│ │ ├── meson.build
│ │ └── password_login_page.ui
│ └── meson.build
├── settings_window.py
└── typing_indicator.py
├── mockups
└── mirdorph.svg
├── org.gnome.gitlab.ranchester.Mirdorph.json
├── patches
├── discord.py
│ ├── 0001-fix-broken-.content-and-.embeds-and-more-in-self-bot.patch
│ └── 0002-fix-on-typing-for-self-bots.patch
├── html2pango
│ └── 0001-allow-usage-from-python.patch
└── libadwaita
│ └── 0001-revert-stylesheet-softer-window-shadows.patch
├── po
├── LINGUAS
├── POTFILES.in
├── README.md
├── lt.po
├── meson.build
└── update_potfiles.sh
└── tests
├── load_gtk.py
├── test_confman.py
├── test_message_parsing.py
└── test_plug_engine.py
/.gitignore:
--------------------------------------------------------------------------------
1 | **/__pycache__
2 | build-dir/
3 | .flatpak-builder/
4 | .flatpak
5 | .pytest_cache/
6 | unmeson-mirdorph.gresource.xml
7 | unmeson-mirdorph.gresource
8 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | include: 'https://gitlab.gnome.org/GNOME/citemplates/raw/master/flatpak/flatpak_ci_initiative.yml'
2 |
3 | variables:
4 | BUNDLE: "mirdorph.flatpak"
5 |
6 | flatpak:
7 | image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
8 | before_script:
9 | - "flatpak install --user -y --noninteractive flathub-beta org.freedesktop.Sdk.Extension.rust-stable//21.08beta"
10 | variables:
11 | MANIFEST_PATH: "org.gnome.gitlab.ranchester.Mirdorph.json"
12 | FLATPAK_MODULE: "mirdorph"
13 | RUNTIME_REPO: "https://flathub.org/repo/flathub.flatpakrepo"
14 | APP_ID: "org.gnome.gitlab.ranchester.Mirdorph"
15 | extends: .flatpak
16 |
17 | flatpak-test:
18 | image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:master'
19 | before_script:
20 | - "flatpak install --user -y --noninteractive flathub-beta org.freedesktop.Sdk.Extension.rust-stable//21.08beta"
21 | script:
22 | - !reference [.flatpak, script]
23 | - 'flatpak install -y --noninteractive --user --no-pull --no-related mirdorph.flatpak'
24 | - 'flatpak run --command="python3" --cwd=$(pwd)/tests --filesystem=$(pwd) org.gnome.gitlab.ranchester.Mirdorph -m pytest -v --junitxml=../report.xml'
25 | variables:
26 | MANIFEST_PATH: "org.gnome.gitlab.ranchester.Mirdorph.json"
27 | FLATPAK_MODULE: "mirdorph"
28 | RUNTIME_REPO: "https://flathub.org/repo/flathub.flatpakrepo"
29 | APP_ID: "org.gnome.gitlab.ranchester.Mirdorph"
30 | extends: .flatpak
31 | artifacts:
32 | when: always
33 | reports:
34 | junit: report.xml
35 |
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mirdorph - a crappy low feature Discord Client
2 |
3 | I don't have much else to add.
4 |
5 | ## To run:
6 |
7 | ### Download latest snapshot from CI (x86.64 only):
8 |
9 | 1. Head over to [the *Jobs* page of the repository](https://gitlab.gnome.org/ranchester/mirdorph/-/jobs)
10 | 2. Find the latest one with the label *flatpak*
11 | 3. Press the download button on the right
12 | 4. Extract the zip you downloaded and open a terminal in the resulting directory
13 | 5. Install the snapshot with `flatpak --user install mirdorph.flatpak`
14 |
15 | ### Build from source method:
16 |
17 | Have flatpak and flatpak-builder installed.
18 | Install `org.gnome.Sdk`, `org.gnome.Platform` (version master) and `org.freedesktop.Sdk.Extension.rust-stable` if they are
19 | not allready installed.
20 |
21 | And from the source directory:
22 |
23 | ```bash
24 | # To build and install
25 | flatpak-builder --force-clean --user --install build-dir org.gnome.gitlab.ranchester.Mirdorph.json
26 | # To run
27 | flatpak run org.gnome.gitlab.ranchester.Mirdorph
28 | ```
29 |
30 | ## Screenshots
31 |
32 | Here are a few screenshots of 0.13.0:
33 |
34 | 
35 | 
36 | 
37 | 
38 | 
39 | 
40 | 
41 | 
42 | 
43 | 
44 | 
45 | 
46 | 
47 |
--------------------------------------------------------------------------------
/build-aux/meson/postinstall.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from os import environ, path
4 | from subprocess import call
5 |
6 | prefix = environ.get("MESON_INSTALL_PREFIX", "/usr/local")
7 | datadir = path.join(prefix, "share")
8 | destdir = environ.get("DESTDIR", "")
9 |
10 | # Package managers set this so we don't need to run
11 | if not destdir:
12 | print("Updating icon cache...")
13 | call(["gtk-update-icon-cache", "-qtf", path.join(datadir, "icons", "hicolor")])
14 |
15 | print("Updating desktop database...")
16 | call(["update-desktop-database", "-q", path.join(datadir, "applications")])
17 |
18 | print("Compiling GSettings schemas...")
19 | call(["glib-compile-schemas", path.join(datadir, "glib-2.0", "schemas")])
20 |
21 |
22 |
--------------------------------------------------------------------------------
/data/icons/hicolor/scalable/apps/org.gnome.gitlab.ranchester.Mirdorph.svg:
--------------------------------------------------------------------------------
1 |
2 |
65 |
--------------------------------------------------------------------------------
/data/icons/hicolor/symbolic/apps/org.gnome.gitlab.ranchester.Mirdorph-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
90 |
--------------------------------------------------------------------------------
/data/icons/meson.build:
--------------------------------------------------------------------------------
1 | scalable_dir = join_paths('hicolor', 'scalable', 'apps')
2 | install_data(
3 | join_paths(scalable_dir, ('@0@.svg').format(app_id)),
4 | install_dir: join_paths(get_option('datadir'), join_paths('icons', scalable_dir))
5 | )
6 |
7 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps')
8 | install_data(
9 | join_paths(symbolic_dir, ('@0@-symbolic.svg').format(app_id)),
10 | install_dir: join_paths(get_option('datadir'), join_paths('icons', symbolic_dir))
11 | )
12 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/chat-communication.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/paper-plane-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
151 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/smile-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
151 |
--------------------------------------------------------------------------------
/data/icons/scalable/status/view-sidebar-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
79 |
--------------------------------------------------------------------------------
/data/meson.build:
--------------------------------------------------------------------------------
1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
2 | moduledir = join_paths(pkgdatadir, 'mirdorph')
3 | gnome = import('gnome')
4 |
5 | subdir('ui')
6 | app_resources = gnome.compile_resources(
7 | 'mirdorph',
8 | 'mirdorph.gresource.xml',
9 | gresource_bundle: true,
10 | install: true,
11 | install_dir: pkgdatadir,
12 | dependencies: [
13 | configure_file(
14 | input: 'ui/about_dialog.ui.in',
15 | output: 'about_dialog.ui',
16 | configuration: glade_conf
17 | )
18 | ]
19 | )
20 |
21 | desktop_file = i18n.merge_file(
22 | input: 'org.gnome.gitlab.ranchester.Mirdorph.desktop.in',
23 | output: 'org.gnome.gitlab.ranchester.Mirdorph.desktop',
24 | type: 'desktop',
25 | po_dir: '../po',
26 | install: true,
27 | install_dir: join_paths(get_option('datadir'), 'applications')
28 | )
29 |
30 | desktop_utils = find_program('desktop-file-validate', required: false)
31 | if desktop_utils.found()
32 | test('Validate desktop file', desktop_utils,
33 | args: [desktop_file]
34 | )
35 | endif
36 |
37 | appstream_file = i18n.merge_file(
38 | input: 'org.gnome.gitlab.ranchester.Mirdorph.appdata.xml.in',
39 | output: 'org.gnome.gitlab.ranchester.Mirdorph.appdata.xml',
40 | po_dir: '../po',
41 | install: true,
42 | install_dir: join_paths(get_option('datadir'), 'appdata')
43 | )
44 |
45 | appstream_util = find_program('appstream-util', required: false)
46 | if appstream_util.found()
47 | test('Validate appstream file', appstream_util,
48 | args: ['validate', appstream_file]
49 | )
50 | endif
51 |
52 | install_data('org.gnome.gitlab.ranchester.Mirdorph.gschema.xml',
53 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
54 | )
55 |
56 | compile_schemas = find_program('glib-compile-schemas', required: false)
57 | if compile_schemas.found()
58 | test('Validate schema file', compile_schemas,
59 | args: ['--strict', '--dry-run', meson.current_source_dir()]
60 | )
61 | endif
62 |
63 | subdir('icons')
64 |
--------------------------------------------------------------------------------
/data/mirdorph.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | style.css
5 |
6 |
7 | icons/scalable/status/org.gnome.gitlab.ranchester.Mirdorph.DiscussionsLogo.svg
8 | icons/scalable/status/org.gnome.gitlab.ranchester.Mirdorph.Guilds-symbolic.svg
9 | icons/scalable/status/view-sidebar-symbolic.svg
10 | icons/scalable/status/smile-symbolic.svg
11 | icons/scalable/status/paper-plane-symbolic.svg
12 | icons/scalable/status/gear-symbolic.svg
13 | icons/scalable/status/chat-communication.svg
14 | ui/login_window.ui
15 | ui/main_window.ui
16 | ui/channel_inner_window.ui
17 | ui/message_entry_bar.ui
18 | ui/channel_sidebar.ui
19 | ui/channel_list_entry.ui
20 | ui/message_view.ui
21 | ui/channel_properties_window.ui
22 | ui/message.ui
23 | ui/generic_attachment.ui
24 | ui/message_entry_bar_attachment.ui
25 | ui/typing_indicator.ui
26 | ui/link_preview.ui
27 | ui/tos_notice.ui
28 | ui/settings_window.ui
29 | ui/extension_row.ui
30 | ui/image_viewer.ui
31 | ui/context_error_dialog.ui
32 |
33 | about_dialog.ui
34 | tos.txt
35 |
36 |
37 |
--------------------------------------------------------------------------------
/data/org.gnome.gitlab.ranchester.Mirdorph.desktop.in:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Mirdorph
3 | Comment=Human Interaction System for Discord
4 | Exec=mirdorph
5 | Icon=org.gnome.gitlab.ranchester.Mirdorph
6 | Terminal=false
7 | Type=Application
8 | Categories=GTK;Network;
9 | Keywords=Discord;Communicate;Discussions;Client;
10 | StartupNotify=true
11 |
--------------------------------------------------------------------------------
/data/org.gnome.gitlab.ranchester.Mirdorph.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/data/style.css:
--------------------------------------------------------------------------------
1 | .login-button {
2 | padding-top: 10px;
3 | padding-bottom: 10px;
4 | border-radius: 9999px;
5 | }
6 |
7 | .password-entry-row {
8 | padding: 10px;
9 | }
10 |
11 | .discord-message {
12 | padding: 3px;
13 | }
14 |
15 | .merged-discord-message {
16 | padding-top: 0px;
17 | padding-bottom: 0px;
18 | }
19 |
20 | .quote {
21 | border-left: 2px solid @accent_bg_color;
22 | color: #888A85;
23 | padding-left: 6px;
24 | }
25 |
26 | .discord-message .username {
27 | color: @accent_color;
28 | font-weight: bold;
29 | font-size: small;
30 | }
31 |
32 | /*
33 | Both the message entry box and the message-history
34 | listbox are in a hdyclamp of equal size. So for them
35 | to align nicely the padding must be equal.
36 |
37 | However, I can't make it too small since I need there
38 | to be room from the message listbox to the border in mobile
39 | mode
40 | */
41 |
42 | .message-history {
43 | background-color: @theme_base_color;
44 | padding: 0 8px 8px;
45 | /* For room for typing indicator, unfortunatej*/
46 | padding-bottom: 20px;
47 | }
48 |
49 | .message-history > row {
50 | border-radius: 7px;
51 | }
52 |
53 | #message-entry-button-box {
54 | margin-left: 11px;
55 | margin-right: 11px;
56 | }
57 |
58 | .message-enforcement-box {
59 | background-color: @theme_base_color;
60 | }
61 |
62 | .channel-banner-box {
63 | padding: 10px;
64 | background-color: @theme_fg_color;
65 | }
66 |
67 | .scroll-button {
68 | padding: 6px;
69 | }
70 |
71 | .reverse-text {
72 | color: @theme_bg_color;
73 | }
74 |
75 | .channel-properties-content-container {
76 | padding: 10px;
77 | }
78 |
79 | .image-attachment-template {
80 | background-color: @theme_base_color;
81 | padding: 5px;
82 | }
83 |
84 | .entry-bar-toolbar {
85 | }
86 |
87 | .message-entry-bar-attachment {
88 | border-radius: 5px;
89 | box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
90 | }
91 |
92 | .message-entry-bar-attachment-container {
93 | padding: 10px;
94 | background-color: @theme_bg_color;
95 | }
96 |
97 | .typing-indicator box {
98 | background-color: @theme_base_color;
99 | border-top-left-radius: 7px;
100 | border-top-right-radius: 7px;
101 | box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px;
102 | }
103 |
104 | .typing-indicator label {
105 | margin-top: 2px;
106 | margin-bottom: 2px;
107 | margin-left: 4px;
108 | margin-right: 4px;
109 | color: @accent_color;
110 | }
111 |
112 | .link_preview row {
113 | padding: 6px;
114 | }
115 |
116 | .link_preview image {
117 | background-color: shade(@theme_base_color, 0.8);
118 | border-radius: 7px;
119 | padding: 10px;
120 | }
121 |
122 | /*
123 | For guild-list, modified autogenerated .content libhandy code.
124 | Basically mimics content without the rounding
125 | */
126 |
127 | list.guild-list, list.guild-list list { background-color: transparent; }
128 |
129 | list.guild-list list.nested > row:not(:active):not(:hover):not(:selected), list.guild-list list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(@theme_bg_color, @theme_base_color, 0.5); }
130 |
131 | list.guild-list list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: mix(@theme_fg_color, @theme_base_color, 0.95); }
132 |
133 | list.guild-list > row:not(.expander):not(:active):not(:hover):not(:selected), list.guild-list > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.guild-list > row.expander row.header:not(:active):not(:hover):not(:selected), list.guild-list > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: @theme_base_color; }
134 |
135 | list.guild-list > row.activatable:not(.expander):not(:active):hover:not(:selected), list.guild-list > row.expander row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(@theme_fg_color, @theme_base_color, 0.95); }
136 |
137 | /* Border color not generalized */
138 | list.guild-list > row, list.guild-list > row list > row { border-color: alpha(@borders, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
139 |
140 | list.guild-list > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
141 |
142 | list.guild-list > row:first-child, list.guild-list > row.expander:first-child row.header, list.guild-list > row.expander:checked, list.guild-list > row.expander:checked row.header, list.guild-list > row.expander:checked + row, list.guild-list > row.expander:checked + row.expander row.header { border-top-left-radius: 0px; border-top-right-radius: 0px; }
143 |
144 | list.guild-list > row:last-child, list.guild-list > row.checked-expander-row-previous-sibling, list.guild-list > row.expander:checked { border-width: 1px; }
145 |
146 | list.guild-list > row:last-child, list.guild-list > row.checked-expander-row-previous-sibling, list.guild-list > row.expander:checked, list.guild-list > row.expander:not(:checked):last-child row.header, list.guild-list > row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.guild-list > row.expander.empty:checked row.header, list.guild-list > row.expander list.nested > row:last-child { border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; }
147 |
148 | list.guild-list > row.expander:checked:not(:first-child), list.guild-list > row.expander:checked + row { margin-top: 6px; }
149 |
150 | /* Remove stupid padding around the channel list */
151 |
152 | .guild-list row .nested row {
153 | padding: 0px;
154 | }
155 |
--------------------------------------------------------------------------------
/data/ui/about_dialog.ui.in:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
--------------------------------------------------------------------------------
/data/ui/channel_inner_window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
17 | vertical
18 | 355
19 |
20 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/data/ui/channel_list_entry.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 10
8 | 10
9 | 10
10 | 10
11 | vertical
12 | 3
13 |
14 |
15 | 0.0
16 | end
17 |
18 |
19 |
20 |
21 | False
22 | 0.0
23 | end
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/data/ui/channel_sidebar.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
127 |
128 |
--------------------------------------------------------------------------------
/data/ui/context_error_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _button_ok
6 |
7 |
8 |
9 |
10 | False
11 | 12
12 | 12
13 | 12
14 | 12
15 | True
16 | 100
17 | 340
18 |
19 |
20 | True
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | OK
30 |
31 |
32 |
33 | _button_ok
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/data/ui/extension_row.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | _is_active_switch
7 |
8 |
9 | 12
10 |
11 |
12 | center
13 | gear-symbolic
14 |
15 |
19 |
20 |
21 |
22 |
23 | center
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/data/ui/generic_attachment.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | none
6 |
7 |
8 |
9 |
10 |
11 | 5
12 | 5
13 | 5
14 | 5
15 | 5
16 |
17 |
18 | vertical
19 |
20 |
21 | unknown.bin
22 | end
23 | 0.0
24 |
27 |
28 |
29 |
30 |
31 | True
32 | end
33 |
34 |
35 |
36 |
37 |
38 |
39 | False
40 |
41 |
42 |
43 | document-save-symbolic
44 |
45 |
46 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/data/ui/image_viewer.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
16 |
17 | vertical
18 | never
19 | False
20 | False
21 | False
22 |
23 |
63 |
64 |
65 |
66 |
67 |
68 | GTK_ALIGN_START
69 | GTK_ALIGN_CENTER
70 |
71 |
72 | 6
73 |
74 |
75 | Loading more media...
76 |
77 |
78 |
79 |
80 | True
81 |
82 |
83 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | center
95 | center
96 |
97 |
98 |
99 |
100 | crossfade
101 |
102 |
103 | GTK_ALIGN_CENTER
104 | 12
105 | 12
106 | 12
107 | 12
108 |
109 |
110 |
111 |
112 |
113 | go-previous-symbolic
114 |
115 |
116 |
119 |
120 |
121 |
122 |
123 | True
124 | end
125 |
126 |
127 |
128 | go-next-symbolic
129 |
130 |
131 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/data/ui/link_preview.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | GTK_ALIGN_START
6 | none
7 |
8 |
9 |
10 |
11 |
12 | 130
13 |
14 |
15 | vertical
16 | 3
17 |
18 |
19 | 140
20 | 140
21 | insert-link-symbolic
22 | 64
23 |
26 |
27 |
28 |
29 |
30 | 14
31 | end
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/data/ui/login_window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 750
6 | 616
7 | org.gnome.gitlab.ranchester.Mirdorph
8 | Connect your Account
9 |
10 |
11 | False
12 | False
13 | True
14 |
15 |
16 | vertical
17 |
18 |
19 | True
20 | 1
21 |
22 |
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 | 1
47 | GTK_ALIGN_CENTER
48 | vertical
49 | 10
50 | 10
51 | 10
52 | 10
53 |
54 |
55 | org.gnome.gitlab.ranchester.Mirdorph.DiscussionsLogo
56 | 256
57 |
58 |
59 |
60 |
61 | Welcome to Mirdorph
62 |
65 |
66 |
67 |
68 |
69 | 10
70 | vertical
71 |
72 |
73 | vertical
74 | 12
75 |
76 |
77 |
78 |
79 | False
80 | GTK_ALIGN_CENTER
81 | 10
82 | 10
83 | Advanced
84 |
85 |
86 | vertical
87 | 10
88 | 12
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | vertical
102 |
103 |
104 |
105 |
123 |
124 |
125 |
126 | True
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/data/ui/meson.build:
--------------------------------------------------------------------------------
1 | glade_conf = configuration_data()
2 | glade_conf.set('VERSION', meson.project_version())
3 | glade_conf.set('APPID', app_id)
4 | glade_conf.set('authorfullname', authorfullname)
5 | glade_conf.set('prettyname', prettyname)
6 | glade_conf.set('CONTRIBUTORS', contributors)
7 |
--------------------------------------------------------------------------------
/data/ui/message.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 5
6 |
7 |
8 | GTK_ALIGN_START
9 |
10 |
11 | 32
12 | True
13 |
14 |
15 |
16 |
17 |
18 |
19 | True
20 | vertical
21 |
22 |
23 | True
24 | 0.0
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | True
36 | vertical
37 | 5
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/data/ui/message_entry_bar.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | vertical
6 |
7 |
8 |
9 |
10 |
11 | 46
12 |
13 |
14 | True
15 | 800
16 | 600
17 |
18 |
19 | message-entry-button-box
20 | 5
21 | GTK_ALIGN_CENTER
22 |
23 |
24 |
25 |
26 | mail-attachment-symbolic
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 | True
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | smile-symbolic
46 |
47 |
48 |
51 |
52 |
53 |
54 |
55 | False
56 |
57 |
58 |
59 | paper-plane-symbolic
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 |
75 | 4
76 |
77 |
78 |
79 |
80 |
81 | vertical
82 |
83 |
84 |
87 |
88 |
89 |
90 |
91 | 2
92 |
93 |
94 | 16
95 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/data/ui/message_entry_bar_attachment.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 115
6 | 115
7 |
8 |
9 |
10 |
11 |
12 | GTK_ALIGN_CENTER
13 | GTK_ALIGN_CENTER
14 | list-add-symbolic
15 |
16 |
17 |
18 |
19 | False
20 |
21 |
22 | True
23 |
24 |
25 | GTK_ALIGN_CENTER
26 | GTK_ALIGN_CENTER
27 | 48
28 | x-office-document-symbolic
29 |
30 |
31 |
32 |
33 | GTK_ALIGN_CENTER
34 | GTK_ALIGN_END
35 | 5
36 | 3
37 | unknown.bin
38 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/data/ui/message_view.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | never
10 | always
11 | 300
12 | 300
13 |
14 |
15 | True
16 | True
17 | 800
18 | 600
19 |
20 |
21 | end
22 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | GTK_ALIGN_END
39 | GTK_ALIGN_END
40 | 24
41 | 24
42 |
43 |
44 | crossfade
45 |
46 |
47 |
48 |
49 |
50 | go-bottom-symbolic
51 |
52 |
53 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/data/ui/settings_window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vertical
7 |
8 |
22 |
23 |
24 |
25 | True
26 |
27 |
28 |
29 |
30 |
31 |
32 | video-display-symbolic
33 | Display
34 |
35 |
36 | Messaging
37 |
38 |
39 | Preview links
40 | Previewing links can decrease performance.
41 | _preview_links_switch
42 |
43 |
44 | GTK_ALIGN_CENTER
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | preferences-system-privacy-symbolic
57 | Privacy
58 |
59 |
60 | Interaction
61 |
62 |
63 | Send typing events
64 | _send_typing_switch
65 |
66 |
67 | GTK_ALIGN_CENTER
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | application-x-addon-symbolic
80 | Extensions
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/data/ui/tos_notice.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Critical Warning
6 | _button_cancel
7 | warning
8 | Use of Mirdorph is a direct violation of Discord's terms of service, and may lead to account termination.
9 |
10 |
11 |
12 |
13 | 12
14 | vertical
15 |
16 |
17 | 10
18 | 10
19 | 10
20 | 400
21 | 200
22 | True
23 |
24 |
25 | False
26 | True
27 |
28 |
29 |
30 |
31 |
32 |
33 | 10
34 | 10
35 | 10
36 | False
37 | I have understood the terms.
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | False
47 | Proceed
48 |
51 |
52 |
53 |
54 |
55 | Cancel
56 |
57 |
58 |
59 | _button_proceed
60 | _button_cancel
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/data/ui/typing_indicator.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | slide-up
6 | GTK_ALIGN_END
7 |
8 |
9 | 2
10 | 800
11 | 600
12 |
13 |
14 | vertical
15 |
16 |
17 | end
18 | 0.0
19 | Noone is typing.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/doc/STANDARD.md:
--------------------------------------------------------------------------------
1 | Mirdorph is a (bad) Discord client utilizing discord.py and GTK3.
2 |
3 | ## How interaction with discord
4 |
5 | First, discord with an asyncio loop runs in the main thread, GTK runs
6 | in a separate thread from the very start, however that doesn't mean
7 | we cannot communicate.
8 |
9 | To run discord code from GTK:
10 |
11 | use asyncio.run_coroutine_threadsafe:
12 |
13 | ```python3
14 | import asyncio
15 |
16 | asyncio.run_coroutine_threadsafe(
17 | discord_client.do_something(),
18 | discord_loop
19 | )
20 | ```
21 |
22 | `discord_loop` and `discord_client` are also attributes of the GtkApplication.
23 | You can get the return value from this with `.result()` (note: is blocking).
24 |
25 | To launch code in Gtk from an async function you pass to the run coroutine thing,
26 | you can use `GLib.idle_add(func, *args)`.
27 |
28 | Now, the other way round:
29 |
30 | There isn't really a reason to do this asside from listening to events, and that is simple.
31 |
32 | ```python3
33 | from .event_receiver import EventReceiver
34 |
35 | class YourObject(EventReceiver):
36 | def __init__(self):
37 | EventReceiver.__init__(self)
38 |
39 | def disc_on_message(self, message):
40 | print(message.content)
41 | ```
42 |
43 | Simply subclass EventReceiver and you will be able to get any Discord event with
44 | `disc_` + the generic event name from the discord.py documentation.
45 |
46 | NOTE: make sure to run discord stuff with asyncio.run_coroutine_threadsafe.
47 | and that is about it with interaction.
48 |
49 | ## Main architect
50 |
51 | The main thing is that any channel/room is a `ChannelInnerWindow` and commonly
52 | refered to as a `context`. The context holds everything needed about a discord channel.
53 | And usually has an integrated `MessageView` which is the thingy that displays the
54 | messages.
55 |
56 | The objects are themselves meant to keep themselves in sync with their on-discord state.
57 |
58 | Messageview at it's heart uses a listview with a Gio.ListStore with GObject "Mobject"
59 | representations of messages, they are sorted automatically, and the row that you see
60 | are rebinded to be any specific message. The widget itself is`MessageWidget`.
61 | Things like the loading spinner are in a separate model, and both are part of a flattened
62 | model.
63 |
64 | A context itself is a Gtk widget too, and is the whole headerbar+area thing you see
65 | on the left. The popoit windows are simpy a `ChannelInnerWindow` inside a `AdwWindow`.
66 |
67 | Global state is application, yada yada, etc. There are quite a few things like that
68 |
69 | Also `ConfMan` is used for configuration, the application has an instance of it as an
70 | attribute. Simply use `confman.get_value("example")` or `confman.set_value("new_value", data)`.
71 |
--------------------------------------------------------------------------------
/doc/asset/mirdorph-channel-properties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-channel-properties.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-guild-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-guild-search.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-image-viewer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-image-viewer.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-loading-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-loading-screen.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-login-gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-login-gui.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-login-password.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-login-password.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-login-token.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-login-token.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-login.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-mobile-with-sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-mobile-with-sidebar.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-mobile.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-popped-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-popped-out.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-unselected-main-win.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-unselected-main-win.png
--------------------------------------------------------------------------------
/doc/asset/mirdorph-with-channel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/doc/asset/mirdorph-with-channel.png
--------------------------------------------------------------------------------
/doc/discord-maintainability.txt:
--------------------------------------------------------------------------------
1 | We are in quite a bad state with the maintainability of our connection to Discord
2 |
3 | First, self bots aren't allowed and the API may change under our feet, and
4 | they can try to prevent it.
5 |
6 | We use `discord.py` which has deprecated self bot support in 1.7 and is removing it in 2.0.
7 |
8 | This is quite a bad state for us, I am currently applying patches on top of 1.7, however I
9 | will have to eventually somehow replace on top of v2.0. discord.py-self fork seems promising,
10 | I think I can work together with them and allows for good communication.
11 |
12 | However there is some community for self bots it seems, and the most real place I found is
13 | r/discord_selfbots. However it seems like many of the community have turned against you,
14 | you can't get help on this in the most popular communication channels like official Discords API
15 | or r/discordapp. Posting a question about self bots on stackoverflow gets you absolutely hated
16 | by morrons trying to mark your issue as a duplicate of an unrelated issue and saying that
17 | "noone will help you here" because it is against the TOS.
18 |
--------------------------------------------------------------------------------
/doc/on_message_in-selfbot-and-other-issues-info-attempt:
--------------------------------------------------------------------------------
1 | What someone has found out about the current issue. (NOTE: this has been worked around)
2 |
3 | > I've been investigating for the past week or so, and from what I can gather
4 | > there are a couple of issues.
5 | >
6 | > 1. Intents need to be disabled. Users don't use intents, and enabling
7 | > intents seems to be one of the causes of this problem. Discord may have
8 | > even done it on purpose.
9 | >
10 | > 2. The IDENTIFY packet sent needs to be changed. To log on to the API,
11 | > whatever is using it (be it discord.py or the official Discord client),
12 | > sends an IDENTIFY packet with the token and some other info. These are
13 | > different for bot and user, and also seem to be part of the problem.
14 |
15 | > I disabled intents and replaced the IDENTIFY packet in gateway.py, but
16 | > anything that needs intents (e.g. on_message), doesn't fire at all. There's
17 | > either another problem, or there's an internal check that disables things
18 | > that need intents if the specific intent is disabled, even though it
19 | > *does* have
20 | > the correct permissions.
21 |
--------------------------------------------------------------------------------
/doc/on_message_self_bots_broken.md:
--------------------------------------------------------------------------------
1 | The various `on_message_*` events are broken in self bots in Discord.py since
2 | a few days ago.
3 |
4 | For example, the .content and .embeds don't work, unless they are messages that you sent!
5 |
6 | Luckily, messages retrieved through .history() still work!
7 |
8 | This basically solves the `on_message` use case, but for editing
9 | we can't really get the before now, only the after. (But who needs before anyways?)
10 |
11 | I really hope Discord doesnt continue breaking this.
12 |
13 | But I have patched discord.py to allways retrieve `on_message` from history.
14 | You can find it in the /patches directory.
15 |
16 | UPDATE:
17 |
18 | There are now kind of real solutions to this, the most promissing is this fork
19 | of discord.py doiflies https://github.com/dolfies/discord.py-self. It originally
20 | fixed the issue of .content and .embeds, has some other things now and more.
21 |
22 | I have backported the fix from it to a nicely formatted patch, and now use
23 | that to fix `on_message` and similar, note that it uses "Lazy Users` in some places.
24 |
25 | I think the community for this is good enough so that this project isn't
26 | likely to suffer a fate worse than death.
27 |
--------------------------------------------------------------------------------
/doc/plugin.md:
--------------------------------------------------------------------------------
1 | You can create basic plugins for this (atleast you should in the future)
2 |
3 | ### Why not libpeas?
4 |
5 | I iniitally tried to use libpeas, and it seemed to be a great solution, already
6 | used by quite a few GNOME apps, no need to reinvent anything.
7 |
8 | However I hit a major issue with how you basically have to define your own GObject
9 | Ginterfaces, (it has a stupid assert to ensure that), and it isn't possible to do
10 | that from Python.
11 |
12 | So I reinvented the wheel, however it should still closely mimick that.
13 |
14 | ### Creating a plugin
15 |
16 | There is an example "helloworld" plugin that is very basic and demonstrates
17 | basic features.
18 |
19 | To create a plugin, you should create it in the `mirdorph/plugins` directory,
20 | first you need a .plugin file there, which looks like this
21 |
22 | ```json
23 | {
24 | "name": "A Plugin",
25 | "module": "aplugin",
26 | "description": "An advanced Plugin",
27 | "built_in": false,
28 | "authors": [
29 | "Your Name"
30 | ]
31 | }
32 | ```
33 |
34 | `name`, `module` and `description` are self-explanatory, however module is the name of the python
35 | module where your plugin is defined, and the directory should also be named after it. The gresource
36 | file also should be named after the module.
37 |
38 | You should use meson to install all the required files.
39 |
40 | The plugin class definition itself has to be a subclass of `MrdPlugin`, however you should use a fitting
41 | subclass of it instead, and check its documentation for more specifics. For example for the most basic plugin
42 | that justs loads at startup and exists on shutdown, you can use `MrdApplicationPlugin`.
43 |
44 | An example would be
45 |
46 | ```python
47 | from mirdorph.plugin import MrdApplicationPlugin
48 |
49 | class HelloWorldPlugin(MrdApplicationPlugin):
50 | def __init__(self):
51 | super().__init__()
52 | ```
53 |
54 | You should also implement the `load` and `unload` methods, and any additional ones that would be needed by the
55 | specific type.
56 |
57 | `load` should actually "enable" the plugin (__init__ shouldn't do that), like showing widgets, enabling functionality,
58 | connecting signals. `unload` should undo **everything** that `load` does.
59 |
60 | ### Extension Sets
61 |
62 | Extension sets are a similar concept to the ones in libpeas (however I am not sure since libpeas has basically zero documentation).
63 |
64 | They are the extension points of the program, some objects have them, they specify the specific type of plugin that they support,
65 | and then it is responsible for loading the plugins when it is notified that a compatible one has shown up/disapeared.
66 |
67 | You can see a basic example in `main.py` with `MrdApplicationPlugin`
68 |
69 | ## CSS
70 |
71 | I just realized I have no way of handling css or icons and stuff.
72 | I guess I could technically just use standard gresurce and use css provider manually, but
73 | I don't know, maybe I will just continue dumping it into the main project css file.
74 |
--------------------------------------------------------------------------------
/doc/slow-window-focusing.txt:
--------------------------------------------------------------------------------
1 | As you may have noticed, focusing/defocusing the window is visibly
2 | slow/laggy. Especially if you have a lot of messages.
3 |
4 | Fractal has this *exact* same issue, and we basically use very similar
5 | code for the message history too.
6 |
7 | You can find out more at the fractal issue https://gitlab.gnome.org/GNOME/fractal/-/issues/525.
8 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project('mirdorph',
2 | version: '0.13.0',
3 | meson_version: '>= 0.50.0',
4 | default_options: [ 'warning_level=2',
5 | ],
6 | )
7 |
8 | i18n = import('i18n')
9 |
10 | description = 'A client for Discord'
11 | prettyname = 'Mirdorph'
12 | authorfullname = 'Raidro Manchester'
13 | app_id = 'org.gnome.gitlab.ranchester.Mirdorph'
14 | contributors = '\n'.join([
15 | 'Raidro Manchester'
16 | ])
17 |
18 | subdir('data')
19 | subdir('mirdorph')
20 | subdir('po')
21 |
22 | meson.add_install_script('build-aux/meson/postinstall.py')
23 |
--------------------------------------------------------------------------------
/mirdorph/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ranchester2/mirdorph/d64ce5a6a23aa768f3ab0d6692c60fe18db42d43/mirdorph/__init__.py
--------------------------------------------------------------------------------
/mirdorph/channel_properties_window.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import asyncio
17 | import datetime
18 | import threading
19 | import discord
20 | from gi.repository import Adw, Gtk, Gio, GLib, Gdk
21 | from .event_receiver import EventReceiver
22 |
23 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/ui/channel_properties_window.ui")
24 | class ChannelPropertiesWindow(Adw.Window):
25 | __gtype_name__ = "ChannelPropertiesWindow"
26 |
27 | _channel_avatar: Adw.Avatar = Gtk.Template.Child()
28 |
29 | _name_label: Gtk.Label = Gtk.Template.Child()
30 | _description_label: Gtk.Label = Gtk.Template.Child()
31 |
32 | _last_activity_button: Gtk.Button = Gtk.Template.Child()
33 |
34 | def __init__(self, channel: discord.TextChannel, *args, **kwargs):
35 | Adw.Window.__init__(self, *args, **kwargs)
36 | self.app = Gio.Application.get_default()
37 | self._channel_disc = channel
38 |
39 | self._name_label.set_label("#" + self._channel_disc.name)
40 | self._channel_avatar.set_text(self._channel_disc.name)
41 | if self._channel_disc.topic:
42 | # New lines mess up the formatting, as this is designed
43 | # for one line.
44 | cleaned_topic = self._channel_disc.topic.replace("\n", "")
45 | self._description_label.set_label(cleaned_topic)
46 | else:
47 | self._description_label.hide()
48 |
49 | threading.Thread(target=self._get_last_activity_time_target).start()
50 |
51 | async def _get_last_activity_time_async_target(self, channel: discord.TextChannel) -> datetime.datetime:
52 | async for message in channel.history(limit=1):
53 | return message.created_at
54 |
55 | def _get_last_activity_time_target(self):
56 | last_activity_time = asyncio.run_coroutine_threadsafe(
57 | self._get_last_activity_time_async_target(self._channel_disc),
58 | self.app.discord_loop
59 | ).result()
60 |
61 | GLib.idle_add(self._set_last_activity_gtk_target, last_activity_time)
62 |
63 | def _set_last_activity_gtk_target(self, time: datetime.datetime):
64 | readable_time = time.strftime("%a. %dd. %Hh. %Mm.")
65 | self._last_activity_button.set_label(readable_time)
66 |
67 | @Gtk.Template.Callback()
68 | def _on_last_activity_button_activate(self, button):
69 | time = self._last_activity_button.get_label()
70 |
71 | clipboard = self.get_clipboard()
72 | clipboard.set(time)
73 |
74 | @Gtk.Template.Callback()
75 | def _on_statistics_button_activate(self, button):
76 | raise NotImplementedError
77 |
78 | @Gtk.Template.Callback()
79 | def _on_back_button_clicked(self, button):
80 | # for mobile the back button is basically
81 | # a close button
82 | self.destroy()
83 |
--------------------------------------------------------------------------------
/mirdorph/confman.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 | #
16 | # Heavily based on Giara code
17 |
18 | import os
19 | import json
20 | import logging
21 | import copy
22 | from pathlib import Path
23 | from gi.repository import GObject
24 |
25 |
26 | class ConfManager(GObject.GObject):
27 | """
28 | The ConfManager is a system that helps manage the configuration
29 | of the application.
30 |
31 | It is recommended to ever only have one instance, and add it to
32 | your application class
33 | """
34 | __gsignals__ = {
35 | "setting_changed": (GObject.SignalFlags.RUN_FIRST, None,
36 | (str,))
37 | }
38 |
39 | BASE_SCHEMA = {
40 | # Example and for testing
41 | "example": 0,
42 | "tos_notice_accepted": False,
43 | "send_typing_events": True,
44 | "preview_links": True,
45 | "enabled_extensions": [
46 | "login_method_graphical",
47 | "login_method_password"
48 | ]
49 | }
50 |
51 | def __init__(self, path: Path = None):
52 | """
53 | Create a ConfManager
54 |
55 | param:
56 | path: (optional) override the default path
57 | """
58 | GObject.GObject.__init__(self)
59 | if path is None:
60 | self.path = Path(
61 | os.environ["XDG_CONFIG_HOME"] + "/" + "mirdorph.conf.json")
62 | else:
63 | self.path = path
64 |
65 | if self.path.is_file():
66 | try:
67 | with open(str(self.path)) as fd:
68 | self._conf = json.load(fd)
69 | # All keys should be known to exist
70 | for k in ConfManager.BASE_SCHEMA:
71 | if k not in self._conf.keys():
72 | if isinstance(
73 | ConfManager.BASE_SCHEMA[k], (list, dict)
74 | ):
75 | self._conf[k] = ConfManager.BASE_SCHEMA[k].copy()
76 | else:
77 | self._conf[k] = ConfManager.BASE_SCHEMA[k]
78 | except:
79 | logging.warning("unknown conf error, resetting conf")
80 | self._conf = ConfManager.BASE_SCHEMA.copy()
81 | self.save_conf()
82 | else:
83 | logging.warning("no conf file found, creating")
84 | self._conf = ConfManager.BASE_SCHEMA.copy()
85 | self.save_conf()
86 |
87 | def save_conf(self):
88 | """
89 | Force save current configuration to disk
90 | """
91 | with open(str(self.path), "w") as fd:
92 | fd.write(json.dumps(self._conf))
93 |
94 | def set_value(self, name: str, val: any):
95 | """
96 | Set a value in the configuration
97 |
98 | You do not need to use save_conf after this
99 | as it is done automatically.
100 |
101 | param:
102 | name: str name of the key, can be any json
103 | serializable object
104 | val: any json serializable value
105 | """
106 | self._conf[name] = copy.deepcopy(val)
107 | self.save_conf()
108 | self.emit("setting_changed", name)
109 |
110 | def get_value(self, name: str) -> any:
111 | return self._conf[name]
112 |
--------------------------------------------------------------------------------
/mirdorph/disc_cogs/meson.build:
--------------------------------------------------------------------------------
1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
2 | moduledir = join_paths(pkgdatadir, 'mirdorph', 'disc_cogs')
3 |
4 | discord_test_cogs_sources = [
5 | 'event_listening.py'
6 | ]
7 |
8 | install_data(discord_test_cogs_sources, install_dir: moduledir)
9 |
--------------------------------------------------------------------------------
/mirdorph/event_manager.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import logging
17 | from .event_receiver import EventReceiver
18 | from gi.repository import Gio, Gtk
19 |
20 | class EventManager:
21 | """
22 | Manages events and event receivers
23 |
24 | You need your application to have one as .event_manager
25 | for the event receivers to register against.
26 | """
27 | def __init__(self):
28 | self.app = Gio.Application.get_default()
29 | self._receivers = []
30 |
31 | def register_receiver(self, receiver: EventReceiver):
32 | """
33 | Register a receiver
34 |
35 | param:
36 | receiver: the receiver object that will now receive e
37 | NOTE: usually users don't use this as the EventReceiver __init__
38 | does it
39 | """
40 | self._receivers.append(receiver)
41 |
42 | def dispatch_event(self, name: str, *args, **kwargs):
43 | logging.info(f"re-dispatcihing event {name} to GLib")
44 | for receiver in self._receivers:
45 | func = getattr(receiver, ("disc_" + name))
46 | func(*args, **kwargs)
47 |
--------------------------------------------------------------------------------
/mirdorph/event_receiver.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import discord
17 | from gi.repository import Gio, Gtk
18 |
19 |
20 | class EventReceiver:
21 | """
22 | An object used to receive Discord events
23 |
24 | To use, subclass it and init it.
25 | Then on functions "disc_" + event name from documentation
26 | you can receive events and all the arguments
27 | """
28 |
29 | def __init__(self):
30 | self._ev_app = Gio.Application.get_default()
31 | self._ev_app.event_manager.register_receiver(self)
32 |
33 | def disc_on_ready(self, *args, **kwargs):
34 | pass
35 |
36 | def disc_on_connect(self, *args, **kwargs):
37 | pass
38 |
39 | def disc_on_shard_connect(self, *args, **kwargs):
40 | pass
41 |
42 | def disc_on_disconnect(self, *args, **kwargs):
43 | pass
44 |
45 | def disc_on_shard_disconnect(self, *args, **kwargs):
46 | pass
47 |
48 | def disc_on_ready(self, *args, **kwargs):
49 | pass
50 |
51 | def disc_on_shard_ready(self, *args, **kwargs):
52 | pass
53 |
54 | def disc_on_resumed(self, *args, **kwargs):
55 | pass
56 |
57 | def disc_on_shard_resumed(self, *args, **kwargs):
58 | pass
59 |
60 | def disc_on_error(self, event, *args, **kwargs):
61 | pass
62 |
63 | def disc_on_socket_raw_receive(self, *args, **kwargs):
64 | pass
65 |
66 | def disc_on_socket_raw_send(self, *args, **kwargs):
67 | pass
68 |
69 | def disc_on_typing(self, *args, **kwargs):
70 | pass
71 |
72 | def disc_on_message(self, *args, **kwargs):
73 | pass
74 |
75 | def disc_on_message_delete(self, *args, **kwargs):
76 | pass
77 |
78 | def disc_on_bulk_message_delete(self, *args, **kwargs):
79 | pass
80 |
81 | def disc_on_raw_message_delete(self, *args, **kwargs):
82 | pass
83 |
84 | def disc_on_message_edit(self, *args, **kwargs):
85 | pass
86 |
87 | def disc_on_raw_message_edit(self, *args, **kwargs):
88 | pass
89 |
90 | def disc_on_reaction_add(self, *args, **kwargs):
91 | pass
92 |
93 | def disc_on_raw_reaction_add(self, *args, **kwargs):
94 | pass
95 |
96 | def disc_on_reaction_remove(self, *args, **kwargs):
97 | pass
98 |
99 | def disc_on_raw_reaction_remove(self, *args, **kwargs):
100 | pass
101 |
102 | def disc_on_reaction_clear(self, *args, **kwargs):
103 | pass
104 |
105 | def disc_on_raw_reaction_clear(self, *args, **kwargs):
106 | pass
107 |
108 | def disc_on_reaction_clear_emoji(self, *args, **kwargs):
109 | pass
110 |
111 | def disc_on_raw_reaction_clear_emoji(self, *args, **kwargs):
112 | pass
113 |
114 | def disc_on_private_channel_delete(self, *args, **kwargs):
115 | pass
116 |
117 | def disc_on_private_channel_create(self, *args, **kwargs):
118 | pass
119 |
120 | def disc_on_private_channel_update(self, *args, **kwargs):
121 | pass
122 |
123 | def disc_on_private_channel_pins_update(self, *args, **kwargs):
124 | pass
125 |
126 | def disc_on_guild_channel_delete(self, *args, **kwargs):
127 | pass
128 |
129 | def disc_on_guild_channel_create(self, *args, **kwargs):
130 | pass
131 |
132 | def disc_on_guild_channel_update(self, *args, **kwargs):
133 | pass
134 |
135 | def disc_on_guild_channel_pins_update(self, *args, **kwargs):
136 | pass
137 |
138 | def disc_on_guild_integrations_update(self, *args, **kwargs):
139 | pass
140 |
141 | def disc_on_webhooks_update(self, *args, **kwargs):
142 | pass
143 |
144 | def disc_on_member_join(self, *args, **kwargs):
145 | pass
146 |
147 | def disc_on_member_remove(self, *args, **kwargs):
148 | pass
149 |
150 | def disc_on_member_update(self, *args, **kwargs):
151 | pass
152 |
153 | def disc_on_user_update(self, *args, **kwargs):
154 | pass
155 |
156 | def disc_on_guild_join(self, *args, **kwargs):
157 | pass
158 |
159 | def disc_on_guild_remove(self, *args, **kwargs):
160 | pass
161 |
162 | def disc_on_guild_update(self, *args, **kwargs):
163 | pass
164 |
165 | def disc_on_guild_role_create(self, *args, **kwargs):
166 | pass
167 |
168 | def disc_on_guild_role_delete(self, *args, **kwargs):
169 | pass
170 |
171 | def disc_on_guild_role_update(self, *args, **kwargs):
172 | pass
173 |
174 | def disc_on_guild_emojis_update(self, *args, **kwargs):
175 | pass
176 |
177 | def disc_on_guild_available(self, *args, **kwargs):
178 | pass
179 |
180 | def disc_on_guild_unavailable(self, *args, **kwargs):
181 | pass
182 |
183 | def disc_on_voice_state_update(self, *args, **kwargs):
184 | pass
185 |
186 | def disc_on_member_ban(self, *args, **kwargs):
187 | pass
188 |
189 | def disc_on_member_unban(self, *args, **kwargs):
190 | pass
191 |
192 | def disc_on_invite_create(self, *args, **kwargs):
193 | pass
194 |
195 | def disc_on_invite_delete(self, *args, **kwargs):
196 | pass
197 |
198 | def disc_on_group_join(self, *args, **kwargs):
199 | pass
200 |
201 | def disc_on_group_remove(self, *args, **kwargs):
202 | pass
203 |
204 | def disc_on_relationship_add(self, *args, **kwargs):
205 | pass
206 |
207 | def disc_on_relationship_remove(self, *args, **kwargs):
208 | pass
209 |
210 | def disc_on_relationship_update(self, *args, **kwargs):
211 | pass
212 |
--------------------------------------------------------------------------------
/mirdorph/link_preview.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import linkpreview
17 | import threading
18 | import logging
19 | import requests
20 | import urllib
21 | import hashlib
22 | import os
23 | import gi
24 | from pathlib import Path
25 | from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
26 |
27 |
28 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/ui/link_preview.ui")
29 | class LinkPreviewExport(Gtk.ListBox):
30 | __gtype_name__ = "LinkPreviewExport"
31 |
32 | _image_dir_path = Path(os.environ["XDG_CACHE_HOME"] / Path("mirdorph"))
33 |
34 | _link_label: Gtk.Label = Gtk.Template.Child()
35 | _link_image: Gtk.Image = Gtk.Template.Child()
36 |
37 | def __init__(self, link: str, *args, **kwargs):
38 | Gtk.ListBox.__init__(self, *args, **kwargs)
39 | self.link = link
40 | self._link_label.set_label(link)
41 |
42 | threading.Thread(target=self._fetch_preview).start()
43 |
44 | def _fetch_preview(self):
45 | try:
46 | preview = linkpreview.link_preview(self.link)
47 | except:
48 | logging.warning(f"could not get preview for {self.link}")
49 | return
50 |
51 | image_path = None
52 | if preview.image:
53 | try:
54 | r = requests.get(preview.image)
55 | except requests.exceptions.MissingSchema:
56 | # Some websites use relative links, which is why this is needed
57 | parsed = urllib.parse.urlparse(self.link)
58 | url_with_schema = parsed.scheme + "://" + parsed.netloc + preview.image
59 | r = requests.get(url_with_schema)
60 |
61 | web_image_path = urllib.parse.urlparse(preview.image).path
62 | ext = os.path.split(web_image_path)[1]
63 | file_hash = hashlib.sha256(preview.image.encode("utf-8")).hexdigest()
64 |
65 | image_path = self._image_dir_path / \
66 | Path(f"link_image_{file_hash}.{ext}")
67 |
68 | with open(image_path, "wb") as f:
69 | f.write(r.content)
70 |
71 | GLib.idle_add(self._display_preview, preview.title, image_path)
72 |
73 | def _display_preview(self, title: str, image_path: str):
74 | if title:
75 | self._link_label.set_label(title)
76 | if image_path:
77 | try:
78 | image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
79 | str(image_path),
80 | # 120 = request(140) - some phantom 20 - the padding (optional??)
81 | 120,
82 | 120,
83 | False
84 | )
85 | except gi.repository.GLib.Error:
86 | logging.warning(f"attempted to load load GIF preview for {self.link}")
87 | return
88 | self._link_image.set_from_pixbuf(image_pixbuf)
89 |
90 | @Gtk.Template.Callback()
91 | def _on_row_activated(self, listbox, row):
92 | Gtk.show_uri_on_window(None, self.link, Gdk.CURRENT_TIME)
93 |
--------------------------------------------------------------------------------
/mirdorph/main_window.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import os
17 | import logging
18 | import threading
19 | import queue
20 | import time
21 | from gi.repository import Adw, Gtk, GLib
22 | from .event_receiver import EventReceiver
23 | from .channel_inner_window import ChannelInnerWindow
24 | from .channel_sidebar import MirdorphChannelSidebar
25 |
26 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/ui/main_window.ui")
27 | class MirdorphMainWindow(Adw.ApplicationWindow, EventReceiver):
28 | __gtype_name__ = "MirdorphMainWindow"
29 |
30 | _loading_stack: Gtk.Stack = Gtk.Template.Child()
31 | _loading_progress_bar: Gtk.ProgressBar = Gtk.Template.Child()
32 |
33 | main_flap: Adw.Flap = Gtk.Template.Child()
34 | # Public, the contexts themselves interact with the stack to manage popout and popin
35 | context_stack: Gtk.Stack = Gtk.Template.Child()
36 | _flap_box: Gtk.Box = Gtk.Template.Child()
37 |
38 | _channel_search_button: Gtk.ToggleButton = Gtk.Template.Child()
39 |
40 | def __init__(self, *args, **kwargs):
41 | Adw.ApplicationWindow.__init__(self, *args, **kwargs)
42 | EventReceiver.__init__(self)
43 |
44 | self._empty_inner_window = ChannelInnerWindow(empty=True)
45 | self.main_flap.connect("notify::folded", self._empty_inner_window.handle_flap_folding)
46 | self.context_stack.add_child(self._empty_inner_window)
47 |
48 | self._channel_sidebar = MirdorphChannelSidebar(channel_search_button=self._channel_search_button, vexpand=True)
49 | self._flap_box.append(self._channel_sidebar)
50 |
51 | GLib.timeout_add(150, self._progress_bar_target)
52 |
53 | def _progress_bar_target(self):
54 | self._loading_progress_bar.pulse()
55 | # We want it to stop pulsing when loading has stopped
56 | return self._loading_stack.get_visible_child_name() == "loading"
57 |
58 | def disc_on_ready(self, *args):
59 | # on_ready shows that overall we are connected, and client.guilds
60 | # becomes available
61 | self._loading_stack.set_visible_child_name("session")
62 |
63 | def _setting_switching_focus_gtk_target(self, context):
64 | try:
65 | context.do_first_see()
66 | except AttributeError:
67 | logging.warning("impossible to set default focus of empty status context")
68 |
69 | @Gtk.Template.Callback()
70 | # When toggling search, the sidebar with the search should always be revealed,
71 | # and the button being active is an indication of that
72 | def _on_channel_search(self, button, gparam):
73 | if self._channel_search_button.get_active():
74 | # When in stack navigation, we don't want to reveal the flap
75 | if self.main_flap.get_folded():
76 | self.main_flap.set_reveal_flap(True)
77 |
78 | @Gtk.Template.Callback()
79 | def _on_context_stack_focus_change(self, stack, strpar):
80 | # I have been trying to set the default focus when swithing to a child to be the entry bar
81 | # However, you can't do it here, as you need to wait for GTK to finish everything,
82 | # which is why GLib.idle_add. And also why the standard focus APIs don't work,
83 | # as the channel is displayed down the line.
84 | GLib.idle_add(self._setting_switching_focus_gtk_target, stack.get_visible_child())
85 |
86 | try:
87 | stack.get_visible_child().load_history()
88 | except AttributeError:
89 | logging.warning("impossible to load history of empty status context")
90 |
91 | @Gtk.Template.Callback()
92 | def _on_window_close(self, window):
93 | self.props.application.quit()
94 |
95 | def reconfigure_for_popout_window(self):
96 | """
97 | Configure the main win for a popout window
98 |
99 | This basically just makes sure the status page
100 | for no channel selected appears.
101 |
102 | NOTE: must be called AFTER you remove the channelcontext
103 | """
104 | self.context_stack.set_visible_child(self._empty_inner_window)
105 |
106 | def unconfigure_popout_window(self, context):
107 | """
108 | Unconfigure the main win for a popout window
109 |
110 | This basically adds it back to the stack and makes it
111 | the currently displayed one
112 |
113 | param:
114 | context - the ChannelInnerWindow context
115 | """
116 | self.context_stack.add_child(context)
117 | self.context_stack.set_visible_child(context)
118 |
119 | def show_active_channel(self, channel_id):
120 | """
121 | Display a specifed channel to the user
122 |
123 | param:
124 | channel_id: int of the channel that you want
125 | to display
126 | """
127 | context = self.props.application.retrieve_inner_window_context(channel_id)
128 | if context.is_poped:
129 | temp_win_top = context.get_native()
130 | if temp_win_top:
131 | temp_win_top.present()
132 |
133 | # May seem weird to use it here, however without it the previous channel
134 | # is shown, and the channel selection breaks.
135 | self.reconfigure_for_popout_window()
136 | else:
137 | # Temp hack to get it to work when switching channels in mobile
138 | # mode
139 | context.handle_flap_folding(self.main_flap, None)
140 |
141 | self.context_stack.set_visible_child(context)
142 | # For making the entry the default focus
143 | context.do_first_see()
144 |
145 |
--------------------------------------------------------------------------------
/mirdorph/meson.build:
--------------------------------------------------------------------------------
1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
2 | moduledir = join_paths(pkgdatadir, 'mirdorph')
3 | plugindir = join_paths(moduledir, 'plugins')
4 | gnome = import('gnome')
5 | python = import('python')
6 |
7 | conf = configuration_data()
8 | conf.set('PYTHON', python.find_installation('python3').path())
9 | conf.set('VERSION', meson.project_version())
10 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir')))
11 | conf.set('pkgdatadir', pkgdatadir)
12 |
13 | configure_file(
14 | input: 'mirdorph.in',
15 | output: 'mirdorph',
16 | configuration: conf,
17 | install: true,
18 | install_dir: get_option('bindir')
19 | )
20 |
21 | mirdorph_sources = [
22 | '__init__.py',
23 | 'main.py',
24 | 'login_window.py',
25 | 'main_window.py',
26 | 'event_manager.py',
27 | 'event_receiver.py',
28 | 'channel_inner_window.py',
29 | 'message_view.py',
30 | 'message.py',
31 | 'message_parsing.py',
32 | 'message_entry_bar.py',
33 | 'attachment.py',
34 | 'channel_sidebar.py',
35 | 'confman.py',
36 | 'channel_properties_window.py',
37 | 'typing_indicator.py',
38 | 'settings_window.py',
39 | 'link_preview.py',
40 | 'image_viewer.py',
41 | 'plugin.py'
42 | ]
43 |
44 | install_data(mirdorph_sources, install_dir: moduledir)
45 |
46 | subdir('disc_cogs')
47 | subdir('plugins')
48 |
--------------------------------------------------------------------------------
/mirdorph/message_parsing.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import copy
17 | import logging
18 | import re
19 | import gi
20 | import html2pango
21 | import mistune
22 | from gi.repository import Adw, Gtk, Gio, Pango
23 | from enum import Enum
24 | from .link_preview import LinkPreviewExport
25 |
26 |
27 | class ComponentType(Enum):
28 | STANDARD = 0
29 | QUOTE = 1
30 |
31 |
32 | def calculate_msg_parts(original_content: str) -> list:
33 | """
34 | Calculate and separate a list of types of components in a str message.
35 |
36 | This isn't low-level formatting stuff, and discord mentions for example,
37 | but top-level stuff like separating quotes out, and probably in the future
38 | codeblocks.
39 |
40 | returns:
41 | `list` of `tuple` where each tupple is (ComponentType, text: str)
42 | """
43 | components = []
44 |
45 | # NOTE: components is a mutable list
46 | def reset_last_component(current_component_type: ComponentType, current_component_text: str,
47 | new_component_type: ComponentType, components: list) -> bool:
48 | if current_component_text and current_component_type != new_component_type:
49 | # Without this, before other parts we have a stupid blank line, (for example quotes)
50 | # For now for all types, and probably for all future ones we also will need this
51 | if current_component_text.endswith("\n"):
52 | current_component_text = current_component_text[:-1]
53 | components.append(
54 | (
55 | copy.deepcopy(current_component_type),
56 | copy.deepcopy(current_component_text)
57 | )
58 | )
59 | # Is this really a new component and we should reset the counter?
60 | return True
61 | return False
62 |
63 | current_component_text = ""
64 | current_component_type = ComponentType.STANDARD
65 | for line in original_content.splitlines(keepends=True):
66 | if line.startswith("> ") or line.startswith(">>> "):
67 | if reset_last_component(current_component_type, current_component_text, ComponentType.QUOTE, components):
68 | current_component_text = ""
69 | current_component_type = None
70 |
71 | current_component_type = ComponentType.QUOTE
72 |
73 | # We don't put the > into the output, because
74 | # it is expected to handle that manually after the fact
75 | amount = len("> ")
76 | if line.startswith(">>>"):
77 | amount = len(">>> ")
78 | current_component_text += line[amount:]
79 | else:
80 | if reset_last_component(current_component_type, current_component_text, ComponentType.STANDARD, components):
81 | current_component_text = ""
82 | current_component_type = None
83 | current_component_type = ComponentType.STANDARD
84 | current_component_text += line
85 |
86 | # When the loop ends, we also need to add the last one
87 | if current_component_text:
88 | components.append(
89 | (
90 | copy.deepcopy(current_component_type),
91 | copy.deepcopy(current_component_text)
92 | )
93 | )
94 |
95 | return components
96 |
97 |
98 | def _extract_discord_components(message_string) -> list:
99 | """
100 | Extract the discord-specific components of a string.
101 | Returns a list of tuples. Where each tuple is:
102 | 0 - discord component type
103 | 1 - the data of the component
104 | 2 - range of chars of the original message string that are the
105 | extracted component
106 |
107 | List empty if none exist
108 | """
109 | # Not immplemented for now, need to figure out embeding widgets in labels.
110 | return []
111 |
112 |
113 | def _generate_exports(message_string: str, include_links: bool=True):
114 | links = []
115 | if include_links:
116 | links = re.findall(r"(?Phttps?://[^\s]+)", message_string)
117 | return [LinkPreviewExport(link) for link in links]
118 |
119 |
120 | def _create_pango_markup(message_string: str) -> str:
121 | html_base = mistune.html(html2pango.html_escape(message_string))
122 | workd_on_str = html2pango.markup_from_raw(html_base)
123 |
124 | return workd_on_str
125 |
126 |
127 | def build_widget_list(message_string: str) -> list:
128 | """
129 | Build a widget list for a part of text in a discord message.
130 | List elements can be either a string of pango markup, or
131 | a custom Gtk Widget
132 |
133 | returns:
134 | A list of either `Gtk.Widget` or `str`
135 | """
136 | # Discord components need to be parsed before converting the strings
137 | # to pango.
138 | discord_components = _extract_discord_components(message_string)
139 | if discord_components:
140 | pass
141 |
142 | widget_list = []
143 | widget_list.append(_create_pango_markup(message_string))
144 |
145 | return widget_list
146 |
147 |
148 | class MessageComponent(Adw.Bin):
149 | def __init__(self, component_content: str, component_type: ComponentType, *args, **kwargs):
150 | Adw.Bin.__init__(self, *args, **kwargs)
151 | self.app = Gio.Application.get_default()
152 | self.component_type = component_type
153 | self._raw_component_content = component_content
154 | # Exports are based on non-sescaped, non-processed content
155 | self.exports = _generate_exports(
156 | self._raw_component_content,
157 | include_links=self.app.confman.get_value("preview_links")
158 | )
159 |
160 | if self.component_type in [ComponentType.STANDARD, ComponentType.QUOTE]:
161 | self._text_label = Gtk.Label(
162 | wrap=True,
163 | wrap_mode=Pango.WrapMode.WORD_CHAR,
164 | selectable=True,
165 | xalign=0.0
166 | )
167 | # Safe currently as only strings
168 | self._text_label.set_markup("".join(build_widget_list(self._raw_component_content)))
169 |
170 | if self.component_type == ComponentType.QUOTE:
171 | self._text_label.add_css_class("quote")
172 |
173 | self.set_child(self._text_label)
174 |
--------------------------------------------------------------------------------
/mirdorph/mirdorph.in:
--------------------------------------------------------------------------------
1 | #!@PYTHON@
2 |
3 | # Copyright 2021 Raidro Manchester
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 |
18 | import os
19 | import sys
20 | import signal
21 | import gettext
22 | import asyncio
23 | import threading
24 | import keyring
25 | import discord
26 | import logging
27 | from discord.ext import commands
28 |
29 | VERSION = '@VERSION@'
30 | pkgdatadir = '@pkgdatadir@'
31 | localedir = '@localedir@'
32 |
33 | sys.path.insert(1, pkgdatadir)
34 | signal.signal(signal.SIGINT, signal.SIG_DFL)
35 |
36 | if __name__ == '__main__':
37 | logging.basicConfig(level=logging.INFO)
38 |
39 | def init_gtk(discord_loop, client, keyring_exists):
40 | logging.info("starting gtk")
41 | # Gettext works for the python part, locale for the xml part.
42 | import gettext
43 | import locale
44 | locale.bindtextdomain("mirdorph", localedir)
45 | locale.textdomain("mirdorph")
46 | gettext.textdomain("mirdorph")
47 | gettext.bindtextdomain("mirdorph", localedir)
48 |
49 | import gi
50 | gi.require_version('Gtk', '4.0')
51 | gi.require_version('Adw', '1')
52 | from gi.repository import Gio, Gtk, Adw
53 |
54 | resource = Gio.Resource.load(os.path.join(pkgdatadir, 'mirdorph.gresource'))
55 | resource._register()
56 |
57 | from mirdorph import main
58 | sys.exit(
59 | main.main(
60 | VERSION,
61 | discord_loop=discord_loop,
62 | discord_client=client,
63 | keyring_exists=keyring_exists
64 | )
65 | )
66 |
67 | def init_discord(discord_client, discord_token):
68 | if discord_token:
69 | cogs = [
70 | 'mirdorph.disc_cogs.event_listening'
71 | ]
72 |
73 | for cog in cogs:
74 | discord_client.load_extension(cog)
75 |
76 | try:
77 | discord_client.run(discord_token, bot=False)
78 | except discord.errors.LoginFailure:
79 | keyring.delete_password("mirdorph", "token")
80 | from gi.repository import GLib, Gio
81 | GLib.idle_add(lambda *_ : Gio.Application.get_default().relaunch())
82 |
83 | logging.info("retrieving token")
84 | discord_token = keyring.get_password("mirdorph", "token")
85 | if discord_token is None:
86 | logging.info("token doesn't exist")
87 | keyring_exists = False
88 | else:
89 | logging.info("token exists")
90 | keyring_exists = True
91 |
92 | # If stuff breaks, drop intents and set fetch_offline_members to False again.
93 | intents = discord.Intents.all()
94 | client = commands.Bot(command_prefix="&&&", intents=intents, max_messages=100000000000000000,
95 | fetch_offline_members=True, guild_subscriptions=True)
96 |
97 | discord_loop = asyncio.get_event_loop()
98 | gtk_thread = threading.Thread(target=init_gtk, args=(
99 | discord_loop, client, keyring_exists))
100 | gtk_thread.start()
101 |
102 | init_discord(client, discord_token)
103 |
--------------------------------------------------------------------------------
/mirdorph/plugins/helloworld/configuration.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | center
7 |
8 |
9 |
10 | 12
11 | 12
12 | vertical
13 | 12
14 |
15 |
16 | Goodbye Message
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/mirdorph/plugins/helloworld/helloworld.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | welcome_message.txt
5 | configuration.ui
6 |
7 |
8 |
--------------------------------------------------------------------------------
/mirdorph/plugins/helloworld/helloworld.plugin:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Hello World",
3 | "module": "helloworld",
4 | "description": "A simple example plugin printing helloworld (or a selectable string) on startup",
5 | "built_in": false,
6 | "authors": [
7 | "Raidro Manchester"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/mirdorph/plugins/helloworld/helloworld.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import gi
17 | from gi.repository import Adw, Gtk, Gio, GObject
18 | from mirdorph.plugin import MrdApplicationPlugin
19 |
20 | BYE_MESSAGE_CONFMAN_KEY = "helloworld_plugin_goodbye_message"
21 | DEFAULT_BYE_MESSAGE = "Bye, World"
22 |
23 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/plugins/helloworld/configuration.ui")
24 | class HelloSettingsPage(Adw.Clamp):
25 | __gtype_name__ = "HelloSettingsPage"
26 |
27 | _message_entry: Gtk.Entry = Gtk.Template.Child()
28 |
29 | def __init__(self, current_goodbye_message: str, *args, **kwargs):
30 | super().__init__(*args, **kwargs)
31 | self.app = Gio.Application.get_default()
32 | self._message_entry.set_text(current_goodbye_message)
33 |
34 | @Gtk.Template.Callback()
35 | def _on_map(self, widget):
36 | self._message_entry.grab_focus()
37 |
38 | @Gtk.Template.Callback()
39 | def _on_message_entry_changed(self, entry):
40 | self.app.confman.set_value(BYE_MESSAGE_CONFMAN_KEY, self._message_entry.get_text())
41 |
42 |
43 | class HelloWorldPlugin(MrdApplicationPlugin):
44 | def __init__(self):
45 | super().__init__()
46 | self.app = Gio.Application.get_default()
47 | self._welcome_message = Gio.resources_lookup_data(
48 | "/org/gnome/gitlab/ranchester/Mirdorph/plugins/helloworld/welcome_message.txt", 0
49 | ).get_data().decode("utf-8")
50 | # Ugly, but since this is the example plugin used for tests, we really don't want to depend
51 | # on external stuff like MirdorphApplication directly
52 | try:
53 | self._goodbye_message = self.app.confman.get_value(BYE_MESSAGE_CONFMAN_KEY)
54 | except AttributeError:
55 | return
56 | except KeyError:
57 | self._goodbye_message = DEFAULT_BYE_MESSAGE
58 | self.app.confman.connect("setting-changed", self._on_confman_setting_changed)
59 |
60 | def _on_confman_setting_changed(self, confman, setting_name: str):
61 | if setting_name == BYE_MESSAGE_CONFMAN_KEY:
62 | self._goodbye_message = self.app.confman.get_value(BYE_MESSAGE_CONFMAN_KEY)
63 |
64 | def load(self):
65 | print(self._welcome_message)
66 |
67 | def unload(self):
68 | print(self._goodbye_message)
69 |
70 | def get_configuration_widget(self):
71 | return HelloSettingsPage(self._goodbye_message)
72 |
--------------------------------------------------------------------------------
/mirdorph/plugins/helloworld/meson.build:
--------------------------------------------------------------------------------
1 | plugin_name = 'helloworld'
2 |
3 | data = [
4 | 'helloworld.py',
5 | 'helloworld.plugin',
6 | 'helloworld.gresource.xml',
7 | 'welcome_message.txt',
8 | 'configuration.ui'
9 | ]
10 |
11 | install_data(data, install_dir: join_paths(plugindir, plugin_name))
12 |
--------------------------------------------------------------------------------
/mirdorph/plugins/helloworld/welcome_message.txt:
--------------------------------------------------------------------------------
1 | Hello World From Gresource!
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_graphical/discord_web_grabber.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import os
17 | import random
18 | import gi
19 | gi.require_version("WebKit2", "5.0")
20 | from gi.repository import Gtk, GObject, WebKit2
21 | from gettext import gettext as _
22 |
23 |
24 | class DiscordGrabber(WebKit2.WebView):
25 | """
26 | A custom Discord Web-based token grabber implementation.
27 |
28 | Created because discordlogin by diamondburned kind of sucks
29 | in some ways.
30 |
31 | To use, create the widget and listen for the "login-complete"
32 | signal, it has the token as an argument.
33 |
34 | The "login-failed" signal is emitted if it fails (comes with
35 | suggested help)
36 | """
37 | __gtype_name__ = "DiscordGrabber"
38 |
39 | __gsignals__ = {
40 | "login_complete": (GObject.SIGNAL_RUN_FIRST, None,
41 | (str,)),
42 | "login_failed": (GObject.SIGNAL_RUN_FIRST, None,
43 | (str,))
44 | }
45 |
46 | LOGIN_TROUBLESHOOT = _("Make sure the window isn't narrow.")
47 |
48 | def __init__(self, *args, **kwargs):
49 | WebKit2.WebView.__init__(self, is_ephemeral=True, *args, **kwargs)
50 | # URI schemes - how to send data from Javascript.
51 | # WebKitGtk doesn't seem to have an easy way to call host python code from
52 | # javascript, however we can register a custom URI scheme,
53 | # and put the data we want to send in the path. And trying to open
54 | # an arbitrary URI is easy in Javascript.
55 |
56 | # WebKit stores registered URI schemes globally, and fails if multiple of the same
57 | # are registered. We can't check which ones are registered easily, however we
58 | # can make them random to make this extremely unlikely.
59 | self.SCHEME_ID = random.randrange(1, 100000)
60 | self._scheme = f"token{str(self.SCHEME_ID)}"
61 |
62 | self.get_context().register_uri_scheme(self._scheme, self._token_uri_callback)
63 |
64 | # The js file can't immediately contain the scheme id, as it is unique
65 | with open(os.path.join(os.path.dirname(__file__), "get_token.js"), "r") as f:
66 | self._injection_code = f"var scheme_id = {self.SCHEME_ID}\n{f.read()}"
67 |
68 | self.connect("resource-load-started", self._on_resource_load_started)
69 |
70 | # The Javascript can not be loaded after the website, as then it is overriden.
71 | # However we can't just run it after load_uri either, as that will still get
72 | # it overriden. Listening to ::load-changed allows us to know when exactly
73 | # the website is fully loaded, which is when we need to inject the Javascript.
74 | self._initial_exec = False
75 | self.connect("load-changed", self._on_load_changed)
76 | self.load_uri("https://discord.com/login")
77 |
78 | def _on_load_changed(self, webview, load_event):
79 | if load_event == WebKit2.LoadEvent.FINISHED and not self._initial_exec:
80 | self._initial_exec = True
81 | self.run_javascript(self._injection_code, None, None, None)
82 |
83 | def _token_uri_callback(self, request: WebKit2.URISchemeRequest):
84 | self.emit("login_complete", request.get_uri()[len(f"{self._scheme}://"):])
85 | # We don't want discord to be continued being displayed
86 | self.load_uri("http://www.blankwebsite.com")
87 |
88 | def _on_resource_load_started(self, webview, resource, request):
89 | # If the token isn't grabbed, we will go on to load the discord Application,
90 | # this indicates failure.
91 | if request.get_uri() == "https://discord.com/app":
92 | self.emit("login_failed", self.LOGIN_TROUBLESHOOT)
93 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_graphical/get_token.js:
--------------------------------------------------------------------------------
1 | let setRequestHeader = XMLHttpRequest.prototype.setRequestHeader
2 | let isAuth = (key, value) => {
3 | return key == "Authorization" && value && !value.startsWith("Bearer");
4 | }
5 |
6 | XMLHttpRequest.prototype.setRequestHeader = function () {
7 | if (isAuth(arguments[0], arguments[1])) {
8 | window.location.href = "token" + scheme_id + "://" + arguments[1]
9 | return
10 | }
11 |
12 | setRequestHeader.apply(this, arguments);
13 | }
14 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_graphical/login_method_graphical.plugin:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Graphical Login",
3 | "module": "login_method_graphical",
4 | "description": "Connect your account with a simple graphical login supporting all methods of authentication.",
5 | "built_in": false,
6 | "authors": [
7 | "Raidro Manchester"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_graphical/login_method_graphical.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import gi
17 | from gettext import gettext as _
18 | from gi.repository import Adw, Gtk
19 | from mirdorph.plugin import MrdLoginMethodPlugin
20 | from .discord_web_grabber import DiscordGrabber
21 |
22 |
23 | class LoginMethodGraphical(MrdLoginMethodPlugin):
24 | def __init__(self):
25 | super().__init__()
26 | self.method_human_name = _("Graphical Login")
27 | self.is_primary = True
28 |
29 | self._login_method_cont = None
30 | self._grabber = None
31 |
32 | def load(self, login_method_cont):
33 | # Useful to directly have a reference of it for rebuilding it on failure
34 | self._login_method_cont = login_method_cont
35 | self._build_token_grabber()
36 |
37 | def unload(self, login_method_cont):
38 | self._grabber = None
39 | self._login_method_cont = None
40 | # We may not have this reference, so we should use the supplied one to avoid
41 | # headaches.
42 | login_method_cont.set_child(None)
43 |
44 | def _build_token_grabber(self):
45 | """
46 | Build the DiscordGrabber and configure it for usage, add it to the method
47 | container.
48 |
49 | This is useful if you need to build it multiple times: for example if
50 | login failed.
51 |
52 | The container is the login method container at `self._login_method_cont`
53 | """
54 | self._grabber = DiscordGrabber()
55 | self._grabber.connect("login-complete", lambda grabber, token : self.emit("token-obtained", token))
56 | self._grabber.connect("login-failed", self._on_login_failed)
57 | self._login_method_cont.set_child(self._grabber)
58 |
59 | def _on_login_failed(self, grabber, help: str):
60 | self._grabber = None
61 | self._login_method_cont.set_child(None)
62 | self._build_token_grabber()
63 |
64 | dialog = Gtk.MessageDialog(
65 | buttons=Gtk.ButtonsType.OK,
66 | text="Login Failed",
67 | secondary_text=help,
68 | modal=True,
69 | transient_for=self._grabber.get_native()
70 | )
71 | dialog.connect("response", lambda *_ : dialog.destroy())
72 | dialog.show()
73 |
74 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_graphical/meson.build:
--------------------------------------------------------------------------------
1 | plugin_name = 'login_method_graphical'
2 |
3 | data = [
4 | 'discord_web_grabber.py',
5 | 'get_token.js',
6 | 'login_method_graphical.py',
7 | 'login_method_graphical.plugin'
8 | ]
9 |
10 | install_data(data, install_dir: join_paths(plugindir, plugin_name))
11 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_manual/login_method_manual.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | manual_login_page.ui
5 |
6 |
7 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_manual/login_method_manual.plugin:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Manual Token Login",
3 | "module": "login_method_manual",
4 | "description": "Connect your account by inserting your authentication token.",
5 | "built_in": true,
6 | "authors": [
7 | "Raidro Manchester"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_manual/login_method_manual.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from __future__ import annotations
17 | import gi
18 | from gettext import gettext as _
19 | from gi.repository import Adw, Gtk, Gio, GLib
20 | from mirdorph.plugin import MrdLoginMethodPlugin
21 |
22 |
23 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/plugins/login_method_manual/manual_login_page.ui")
24 | class ManualLoginPage(Adw.Clamp):
25 | __gtype_name__ = "ManualLoginPage"
26 |
27 | _token_entry: Gtk.Entry = Gtk.Template.Child()
28 | _token_submit_button: Gtk.Button = Gtk.Template.Child()
29 |
30 | def __init__(self, plugin: LoginMethodManual, *args, **kwargs):
31 | Adw.Clamp.__init__(self, *args, **kwargs)
32 | # Way to emit the token obtained signal from here
33 | self._plugin = plugin
34 | self._manual_login_action_group = Gio.SimpleActionGroup()
35 |
36 | self._action_token_submit = Gio.SimpleAction.new("submit", None)
37 | self._action_token_submit.set_enabled(False)
38 | self._action_token_submit.connect("activate", self._on_token_submit)
39 | self._manual_login_action_group.add_action(self._action_token_submit)
40 |
41 | self.insert_action_group("manual-login", self._manual_login_action_group)
42 |
43 | @Gtk.Template.Callback()
44 | def _on_token_entry_changed(self, entry):
45 | self._action_token_submit.set_enabled(
46 | self._token_entry.get_text()
47 | )
48 |
49 | # Not entirely sure if this is how to correctly set the default widgets
50 | # and focus here
51 | @Gtk.Template.Callback()
52 | def _on_map(self, widget):
53 | window = self.get_native()
54 | # GLib.idle_add needed after first map, works correclty without it on first,
55 | # but not on subsequent uses
56 | GLib.idle_add(window.set_default_widget, self._token_submit_button)
57 | # grab_focus returns positive, causing infinite repetition
58 | GLib.idle_add(lambda *_ : self._token_entry.grab_focus() and 0)
59 |
60 | def _on_token_submit(self, *args):
61 | token = self._token_entry.get_text()
62 | self._token_entry.set_text("")
63 | self._plugin.emit("token-obtained", token)
64 |
65 |
66 | class LoginMethodManual(MrdLoginMethodPlugin):
67 | def __init__(self):
68 | super().__init__()
69 | self.method_human_name = _("Manual Token")
70 | self._page = None
71 |
72 | def load(self, login_method_cont):
73 | self._page = ManualLoginPage(self)
74 | login_method_cont.set_child(self._page)
75 |
76 | def unload(self, login_method_cont):
77 | login_method_cont.set_child(None)
78 | self._page = None
79 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_manual/manual_login_page.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | True
7 |
8 |
9 |
10 | vertical
11 | GTK_ALIGN_CENTER
12 | 20
13 | 20
14 | 20
15 | 20
16 | 20
17 |
18 |
19 | Insert Token
20 |
23 |
24 |
25 |
26 |
27 | GTK_ALIGN_CENTER
28 |
29 |
30 | 230
31 | 1
32 |
33 |
34 |
35 |
36 |
37 | manual-login.submit
38 | Insert
39 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_manual/meson.build:
--------------------------------------------------------------------------------
1 | plugin_name = 'login_method_manual'
2 |
3 | data = [
4 | 'login_method_manual.py',
5 | 'login_method_manual.gresource.xml',
6 | 'login_method_manual.plugin',
7 | 'manual_login_page.ui'
8 | ]
9 |
10 | install_data(data, install_dir: join_paths(plugindir, plugin_name))
11 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_password/login_method_password.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | password_login_page.ui
5 |
6 |
7 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_password/login_method_password.plugin:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Password Log In",
3 | "module": "login_method_password",
4 | "description": "Connect your account by entering your email and password.",
5 | "built_in": false,
6 | "authors": [
7 | "Raidro Manchester"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_password/login_method_password.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from __future__ import annotations
17 | import threading
18 | import logging
19 | import gi
20 | import requests
21 | from gettext import gettext as _
22 | from gi.repository import Gtk, Gio, GLib
23 | from mirdorph.plugin import MrdLoginMethodPlugin
24 |
25 |
26 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/plugins/login_method_password/password_login_page.ui")
27 | class PasswordLoginPage(Gtk.Overlay):
28 | __gtype_name__ = "PasswordLoginPage"
29 |
30 | _submit_button: Gtk.Button = Gtk.Template.Child()
31 | _email_entry: Gtk.Entry = Gtk.Template.Child()
32 | _password_entry: Gtk.PasswordEntry = Gtk.Template.Child()
33 |
34 | def __init__(self, plugin: LoginMethodPassword, *args, **kwargs):
35 | Gtk.Overlay.__init__(self, *args, **kwargs)
36 | self.app = Gio.Application.get_default()
37 | # Way to emit the token obtained signal from here
38 | self._plugin = plugin
39 | self._plugin.headerbar.pack_end(self._submit_button)
40 |
41 | # Hack:
42 | # action groups don't work on non-children, which this button is due to the headerbar separation,
43 | # so instead we add it to the global application, and use a fake prefix to indicate that this really
44 | # isn't an application-global action.
45 | self._action_password_submit = Gio.SimpleAction.new("fake-prefix-password-login-submit", None)
46 | self._action_password_submit.set_enabled(False)
47 | self._action_password_submit.connect("activate", self._on_submit)
48 | self.app.add_action(self._action_password_submit)
49 |
50 | @Gtk.Template.Callback()
51 | def _on_warning_bar_response(self, bar: Gtk.InfoBar, response_id: Gtk.ResponseType):
52 | if response_id == Gtk.ResponseType.CLOSE:
53 | bar.hide()
54 |
55 | @Gtk.Template.Callback()
56 | def _on_email_entry_activate(self, entry):
57 | if self._email_entry.get_text():
58 | self._password_entry.grab_focus()
59 |
60 | @Gtk.Template.Callback()
61 | def _on_credentials_entries_changed(self, entry):
62 | self._action_password_submit.set_enabled(
63 | self._email_entry.get_text() and self._password_entry.get_text()
64 | )
65 |
66 | @Gtk.Template.Callback()
67 | def _on_map(self, widget):
68 | window = self.get_native()
69 | window.set_default_widget(self._submit_button)
70 |
71 | @Gtk.Template.Callback()
72 | def _on_unmap(self, widget):
73 | self._plugin.headerbar.remove(self._submit_button)
74 |
75 | def _on_submit(self, *args):
76 | window = self.get_native()
77 | window.set_sensitive(False)
78 | threading.Thread(target=self._token_retrieval_target).start()
79 |
80 | def _token_retrieval_target(self):
81 | email = self._email_entry.get_text()
82 | password = self._password_entry.get_text()
83 | payload = {
84 | "login": email,
85 | "password": password
86 | }
87 | r = requests.post(
88 | "https://discord.com/api/v9/auth/login", json=payload)
89 | if "token" in r.json():
90 | GLib.idle_add(self._plugin.emit, "token-obtained", r.json()["token"])
91 | else:
92 | logging.fatal(
93 | "Token not found in Discord Password login response, login failed. Incorrect password?")
94 | GLib.idle_add(self.app.relaunch)
95 |
96 |
97 | class LoginMethodPassword(MrdLoginMethodPlugin):
98 | def __init__(self):
99 | super().__init__()
100 | self.method_human_name = _("Username and Password")
101 | self._page = None
102 |
103 | def load(self, login_method_cont):
104 | self._page = PasswordLoginPage(self)
105 | login_method_cont.set_child(self._page)
106 |
107 | def unload(self, login_method_cont):
108 | login_method_cont.set_child(None)
109 | self._page = None
110 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_password/meson.build:
--------------------------------------------------------------------------------
1 | plugin_name = 'login_method_password'
2 |
3 | data = [
4 | 'login_method_password.py',
5 | 'login_method_password.gresource.xml',
6 | 'login_method_password.plugin',
7 | 'password_login_page.ui'
8 | ]
9 |
10 | install_data(data, install_dir: join_paths(plugindir, plugin_name))
11 |
--------------------------------------------------------------------------------
/mirdorph/plugins/login_method_password/password_login_page.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | app.fake-prefix-password-login-submit
8 | Submit
9 |
12 |
13 | 1
14 |
15 |
16 |
17 |
18 | GTK_ALIGN_START
19 | warning
20 | True
21 |
22 |
23 |
24 | 16
25 |
26 |
27 | vertical
28 |
29 |
30 | 1
31 | 0.0
32 | WARNING: Email and password login not recommended:
33 |
34 |
35 |
36 |
37 | 1
38 | 0.0
39 | Will not work with advanced configurations (2FA/Captcha), use other methods instead.
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 450
51 |
52 |
53 | vertical
54 | GTK_ALIGN_CENTER
55 | 20
56 | 20
57 | 20
58 | 20
59 | 20
60 |
61 |
62 | Password Login
63 |
66 |
67 |
68 |
69 |
70 | none
71 |
72 |
73 |
74 |
75 | Email Adress
76 |
77 |
78 |
79 |
80 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | Password
90 | True
91 | True
92 |
93 |
94 |
95 |
98 |
99 |
100 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/mirdorph/plugins/meson.build:
--------------------------------------------------------------------------------
1 | plugins = [
2 | 'helloworld',
3 | 'login_method_graphical',
4 | 'login_method_manual',
5 | 'login_method_password'
6 | ]
7 |
8 | foreach plugin : plugins
9 | subdir(plugin)
10 | endforeach
11 |
--------------------------------------------------------------------------------
/mirdorph/settings_window.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | from gettext import gettext as _
17 | from gi.repository import Adw, Gtk, Gio, GObject
18 | from mirdorph.plugin import MrdPluginInfo
19 |
20 |
21 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/ui/extension_row.ui")
22 | class ExtensionRow(Adw.ActionRow):
23 | __gtype_name__ = "ExtensionRow"
24 |
25 | plugin = GObject.Property(type=MrdPluginInfo)
26 |
27 | _settings_button: Gtk.Button = Gtk.Template.Child()
28 | _is_active_switch: Gtk.Switch = Gtk.Template.Child()
29 |
30 | def __init__(self, plugin: MrdPluginInfo, preferences_window: Adw.PreferencesWindow, *args, **kwargs):
31 | Adw.ExpanderRow.__init__(self, *args, **kwargs)
32 | self.plugin = plugin
33 | self._preferences_window = preferences_window
34 | # GtkExpression directly in the UI file would be better, however after a lot
35 | # of trying I found out it doesn't work in PyGObject. When it gets support,
36 | # this should be easily changable to and in the UI file
37 | self.plugin.bind_property("name", self, "title", GObject.BindingFlags.SYNC_CREATE)
38 | self.plugin.bind_property("description", self, "subtitle", GObject.BindingFlags.SYNC_CREATE)
39 | self.plugin.bind_property("configurable", self._settings_button, "sensitive", GObject.BindingFlags.SYNC_CREATE)
40 | # SYNC_CREATE alone isn't enough for changing the property here to change it on the plugin object,
41 | # so for properties that can change at runtime, binding them again with different flags is needed too.
42 | self.plugin.bind_property("active", self._is_active_switch, "active", GObject.BindingFlags.SYNC_CREATE)
43 | self.plugin.bind_property("active", self._is_active_switch, "active", GObject.BindingFlags.BIDIRECTIONAL)
44 |
45 | @Gtk.Template.Callback()
46 | def _on_settings_button_clicked(self, button):
47 | self._preferences_window.present_extension_configuration(self.plugin)
48 |
49 |
50 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/ui/settings_window.ui")
51 | class MirdorphSettingsWindow(Adw.PreferencesWindow):
52 | __gtype_name__ = "MirdorphSettingsWindow"
53 |
54 | _send_typing_switch: Gtk.Switch = Gtk.Template.Child()
55 | _preview_links_switch: Gtk.Switch = Gtk.Template.Child()
56 |
57 | _extensions_pref_group: Adw.PreferencesGroup = Gtk.Template.Child()
58 |
59 | _configuration_page: Gtk.Box = Gtk.Template.Child()
60 | _configuration_window_title: Adw.WindowTitle = Gtk.Template.Child()
61 | _configuration_content: Adw.Bin = Gtk.Template.Child()
62 |
63 | def __init__(self, *args, **kwargs):
64 | Adw.PreferencesWindow.__init__(self, *args, **kwargs)
65 | self._init_values()
66 | self._init_extensions()
67 |
68 | def _init_values(self):
69 | self._send_typing_switch.set_state(
70 | self.props.application.confman.get_value("send_typing_events")
71 | )
72 | self._preview_links_switch.set_state(
73 | self.props.application.confman.get_value("preview_links")
74 | )
75 |
76 | def _init_extensions(self):
77 | for plugin in self.props.application.plugin_engine.get_available_plugins():
78 | if not plugin.built_in:
79 | extension_row = ExtensionRow(plugin, self)
80 | self._extensions_pref_group.add(extension_row)
81 |
82 | def present_extension_configuration(self, plugin: MrdPluginInfo):
83 | """
84 | Present the configuration settings of a configurable plugin to
85 | the user.
86 |
87 | param:
88 | plugin: the configurable `MrdPluginInfo` settings' you want to present
89 | """
90 | self._configuration_window_title.set_title(_("{} Settings").format(plugin.name))
91 | self._configuration_content.set_child(plugin.u_activatable.get_configuration_widget())
92 | self.present_subpage(
93 | self._configuration_page
94 | )
95 |
96 | @Gtk.Template.Callback()
97 | def _on_configuration_close(self, *args):
98 | self.close_subpage()
99 |
100 | @Gtk.Template.Callback()
101 | def _on_send_typing_switch_state_set(self, switch: Gtk.Switch, state: bool):
102 | self.props.application.confman.set_value("send_typing_events", state)
103 |
104 | @Gtk.Template.Callback()
105 | def _on_preview_links_switch_state_set(self, switch: Gtk.Switch, state: bool):
106 | self.props.application.confman.set_value("preview_links", state)
107 |
--------------------------------------------------------------------------------
/mirdorph/typing_indicator.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 Raidro Manchester
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import discord
17 | import time
18 | import threading
19 | import datetime
20 | from gettext import gettext as _
21 | from gi.repository import Gtk, GLib
22 | from xml.sax.saxutils import escape as escape_xml
23 | from .event_receiver import EventReceiver
24 |
25 |
26 | @Gtk.Template(resource_path="/org/gnome/gitlab/ranchester/Mirdorph/ui/typing_indicator.ui")
27 | class TypingIndicator(Gtk.Revealer, EventReceiver):
28 | __gtype_name__ = "TypingIndicator"
29 |
30 | _typing_label: Gtk.Label = Gtk.Template.Child()
31 |
32 | def __init__(self, channel: discord.channel.TextChannel, *args, **kwargs):
33 | Gtk.Revealer.__init__(self, *args, **kwargs)
34 | EventReceiver.__init__(self)
35 | self._channel = channel
36 |
37 | self._currently_typing_users = []
38 | # Basically saving when the last typing event was received,
39 | # so we know when to stop displaying it.
40 | self._times_of_user_typings = []
41 |
42 | def _wating_for_type_end_target(self, user: discord.User, when: datetime.datetime):
43 | # Even if we wanted this to be smaller, we can't decrease it too much as discord itself
44 | # only sends the typing event every so often.
45 | time.sleep(10)
46 | for tims in [typing_event[1] for typing_event in self._times_of_user_typings if typing_event[0] == user]:
47 | if tims > when:
48 | return
49 |
50 | if user in self._currently_typing_users:
51 | self._currently_typing_users.remove(user)
52 | GLib.idle_add(self._sync_typing_label)
53 |
54 | def _sync_typing_label(self):
55 | if self._currently_typing_users:
56 | self.set_reveal_child(True)
57 | username_list = ", ".join(
58 | [f"{escape_xml(user.name)}" for user in self._currently_typing_users]
59 | )
60 | if len(self._currently_typing_users) >= 2:
61 | typing_info_message = _("%s are typing...") % username_list
62 | else:
63 | typing_info_message = _("%s is typing...") % username_list
64 | self._typing_label.set_markup(typing_info_message)
65 | else:
66 | self.set_reveal_child(False)
67 | self._typing_label.set_label(_("Noone is typing."))
68 |
69 | def disc_on_message(self, message: discord.Message):
70 | if message.channel == self._channel and message.author in self._currently_typing_users:
71 | self._currently_typing_users.remove(message.author)
72 | self._sync_typing_label()
73 |
74 | def disc_on_typing(self, channel: discord.channel.TextChannel, user: discord.User, when: datetime.datetime):
75 | if user == self._channel.guild.me:
76 | return
77 |
78 | if channel.id == self._channel.id:
79 | if user not in self._currently_typing_users:
80 | self._currently_typing_users.append(user)
81 | self._sync_typing_label()
82 | self._times_of_user_typings.append((user, when))
83 | threading.Thread(target=self._wating_for_type_end_target, args=(user, when)).start()
84 |
--------------------------------------------------------------------------------
/patches/discord.py/0001-fix-broken-.content-and-.embeds-and-more-in-self-bot.patch:
--------------------------------------------------------------------------------
1 | From 476382f788fdab9dc3e6a3723f753a03e2de10bf Mon Sep 17 00:00:00 2001
2 | From: Raidro Manchester
3 | Date: Sat, 15 May 2021 16:02:51 +0300
4 | Subject: [PATCH] fix broken .content and .embeds and more in self bots
5 |
6 | To fix broken .content and .embeds, the main thing you need to do is to
7 | drop all intents before sending identify, and to substitute relasonship
8 | and recipient data when it randomly breaks. Then everything (as far as I
9 | can see) works again
10 |
11 | We do not need to change user agent or the identify packet more as the
12 | work that this patch is based on does. It is unncessary.
13 |
14 | We also warn when substituting the data with "LazyUser".
15 | ---
16 | discord/channel.py | 12 +++++++++++-
17 | discord/gateway.py | 4 +++-
18 | discord/relationship.py | 9 ++++++++-
19 | discord/state.py | 17 ++++++++++++++++-
20 | discord/user.py | 14 ++++++++++++++
21 | 5 files changed, 52 insertions(+), 4 deletions(-)
22 |
23 | diff --git a/discord/channel.py b/discord/channel.py
24 | index bd4a39dc..b5322a2b 100644
25 | --- a/discord/channel.py
26 | +++ b/discord/channel.py
27 | @@ -26,6 +26,7 @@ DEALINGS IN THE SOFTWARE.
28 |
29 | import time
30 | import asyncio
31 | +import logging
32 |
33 | import discord.abc
34 | from .permissions import Permissions
35 | @@ -35,6 +36,8 @@ from . import utils
36 | from .asset import Asset
37 | from .errors import ClientException, NoMoreItems, InvalidArgument
38 |
39 | +log = logging.getLogger(__name__)
40 | +
41 | __all__ = (
42 | 'TextChannel',
43 | 'VoiceChannel',
44 | @@ -1211,7 +1214,14 @@ class DMChannel(discord.abc.Messageable, Hashable):
45 |
46 | def __init__(self, *, me, state, data):
47 | self._state = state
48 | - self.recipient = state.store_user(data['recipients'][0])
49 | + # Why? For some reason it complains about it not existing,
50 | + # so we workaround that by using lazy users
51 | + if 'recipients' in data:
52 | + self.recipient = state.store_user(data['recipients'][0])
53 | + else:
54 | + log.warning('recipients data not available in payload, doing lazy')
55 | + self.recipient = state.store_lazy_user(data['recipient_ids'][0])
56 | +
57 | self.me = me
58 | self.id = int(data['id'])
59 |
60 | diff --git a/discord/gateway.py b/discord/gateway.py
61 | index 210a8822..7647b591 100644
62 | --- a/discord/gateway.py
63 | +++ b/discord/gateway.py
64 | @@ -396,7 +396,9 @@ class DiscordWebSocket:
65 | }
66 |
67 | if state._intents is not None:
68 | - payload['d']['intents'] = state._intents.value
69 | + # If intents are sent as self bot, various message stuff
70 | + # breaks completely
71 | + pass
72 |
73 | await self.call_hooks('before_identify', self.shard_id, initial=self._initial_identify)
74 | await self.send_as_json(payload)
75 | diff --git a/discord/relationship.py b/discord/relationship.py
76 | index 0a9fffda..b309f134 100644
77 | --- a/discord/relationship.py
78 | +++ b/discord/relationship.py
79 | @@ -24,9 +24,12 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
80 | DEALINGS IN THE SOFTWARE.
81 | """
82 |
83 | +import logging
84 | from .enums import RelationshipType, try_enum
85 | from . import utils
86 |
87 | +log = logging.getLogger(__name__)
88 | +
89 | class Relationship:
90 | """Represents a relationship in Discord.
91 |
92 | @@ -48,7 +51,11 @@ class Relationship:
93 | def __init__(self, *, state, data):
94 | self._state = state
95 | self.type = try_enum(RelationshipType, data['type'])
96 | - self.user = state.store_user(data['user'])
97 | + if 'user' in data:
98 | + self.user = state.store_user(data['user'])
99 | + else:
100 | + log.warning('relasionship data for user %s not available in payload, doing lazy' % data['user_id'])
101 | + self.user = state.store_lazy_user(data['user_id'])
102 |
103 | def __repr__(self):
104 | return ''.format(self)
105 | diff --git a/discord/state.py b/discord/state.py
106 | index da1212c1..5978f132 100644
107 | --- a/discord/state.py
108 | +++ b/discord/state.py
109 | @@ -39,7 +39,7 @@ import os
110 |
111 | from .guild import Guild
112 | from .activity import BaseActivity
113 | -from .user import User, ClientUser
114 | +from .user import User, ClientUser, LazyUser
115 | from .emoji import Emoji
116 | from .mentions import AllowedMentions
117 | from .partial_emoji import PartialEmoji
118 | @@ -280,6 +280,21 @@ class ConnectionState:
119 | self._users[user_id] = user
120 | return user
121 |
122 | + # Self bots in certain places may randomly not get the
123 | + # required data in the payload. In that case,
124 | + # we substitute it with this and give them a warning.
125 | + # Doesn't seem to break anything
126 | + def store_lazy_user(self, user_id):
127 | + # significantly (300%) faster than setdefault
128 | + user_id = int(user_id)
129 | + try:
130 | + return self._users[user_id]
131 | + except KeyError:
132 | + user = LazyUser(state=self, user_id=user_id)
133 | + if user.discriminator != '0000':
134 | + self._users[user_id] = user
135 | + return user
136 | +
137 | def store_user_no_intents(self, data):
138 | return User(state=self, data=data)
139 |
140 | diff --git a/discord/user.py b/discord/user.py
141 | index 8b2d3a1c..9426c5bf 100644
142 | --- a/discord/user.py
143 | +++ b/discord/user.py
144 | @@ -957,3 +957,17 @@ class User(BaseUser, discord.abc.Messageable):
145 | mutual_guilds=mutual_guilds,
146 | user=self,
147 | connected_accounts=data['connected_accounts'])
148 | +
149 | +# Private lazy user for self bots with substitute data,
150 | +# for when receiving real users in recipients and relationships
151 | +# throws random errors
152 | +class LazyUser(BaseUser):
153 | + def __init__(self, state, user_id):
154 | + fake_data = {
155 | + 'username': None,
156 | + 'id': user_id,
157 | + 'discriminator': 'BUMBA',
158 | + 'avatar': None,
159 | + }
160 | +
161 | + super().__init__(state=state, data=fake_data)
162 | --
163 | 2.30.2
164 |
165 |
--------------------------------------------------------------------------------
/patches/discord.py/0002-fix-on-typing-for-self-bots.patch:
--------------------------------------------------------------------------------
1 | From baf3848c225ccaefb0f5ac078e2ce6f063d4540e Mon Sep 17 00:00:00 2001
2 | From: Raidro Manchester
3 | Date: Tue, 1 Jun 2021 22:01:27 +0300
4 | Subject: [PATCH] fix on_typing for self bots
5 |
6 | For `on_typing` to fire for self bots, it is required to send opcode 14
7 | to the guild in question.
8 |
9 | This does that, which is apparently used by the official client to
10 | signify when it is "viewing" the guild.
11 |
12 | Apparently you should ensure that the channel is a text channel first,
13 | which I did. Based on doflies
14 | ---
15 | discord/client.py | 2 ++
16 | discord/gateway.py | 33 ++++++++++++++++++++++++++++++++-
17 | 2 files changed, 34 insertions(+), 1 deletion(-)
18 |
19 | diff --git a/discord/client.py b/discord/client.py
20 | index 1c35fddf..a186b336 100644
21 | --- a/discord/client.py
22 | +++ b/discord/client.py
23 | @@ -269,6 +269,8 @@ class Client:
24 | await self.ws.request_sync(guilds)
25 |
26 | def _handle_ready(self):
27 | + for guild in self.guilds:
28 | + self.ws.subscribe_to_guild_events(guild)
29 | self._ready.set()
30 |
31 | @property
32 | diff --git a/discord/gateway.py b/discord/gateway.py
33 | index 7647b591..bf75471d 100644
34 | --- a/discord/gateway.py
35 | +++ b/discord/gateway.py
36 | @@ -40,7 +40,7 @@ import aiohttp
37 |
38 | from . import utils
39 | from .activity import BaseActivity
40 | -from .enums import SpeakingState
41 | +from .enums import SpeakingState, ChannelType
42 | from .errors import ConnectionClosed, InvalidArgument
43 |
44 | log = logging.getLogger(__name__)
45 | @@ -249,6 +249,8 @@ class DiscordWebSocket:
46 | a connection issue.
47 | GUILD_SYNC
48 | Send only. Requests a guild sync.
49 | + LAZY_GUILD_REQUEST
50 | + Send only. Subscribes you to guilds. Responds with GUILD_MEMBER_LIST_UPDATE sync.
51 | gateway
52 | The gateway we are currently connected to.
53 | token
54 | @@ -268,6 +270,7 @@ class DiscordWebSocket:
55 | HELLO = 10
56 | HEARTBEAT_ACK = 11
57 | GUILD_SYNC = 12
58 | + LAZY_GUILD_REQUEST = 14
59 |
60 | def __init__(self, socket, *, loop):
61 | self.socket = socket
62 | @@ -418,6 +421,34 @@ class DiscordWebSocket:
63 | await self.send_as_json(payload)
64 | log.info('Shard ID %s has sent the RESUME payload.', self.shard_id)
65 |
66 | + def subscribe_to_guild_events(self, guild):
67 | + """Sends opcode 14 to guild."""
68 | + # The channel we use must be a text or news channel
69 | + first_valid_channel = None
70 | + for channel in guild.channels:
71 | + if channel.type in (ChannelType.news, ChannelType.text):
72 | + first_valid_channel = channel
73 | +
74 | + payload = {
75 | + "op": self.LAZY_GUILD_REQUEST,
76 | + "d": {
77 | + "guild_id": str(guild.id),
78 | + "typing": True,
79 | + "threads": False,
80 | + "activities": True,
81 | + "members": [],
82 | + "channels": {
83 | + str(first_valid_channel.id): [
84 | + [
85 | + 0,
86 | + 99
87 | + ]
88 | + ]
89 | + }
90 | + }
91 | + }
92 | + asyncio.ensure_future(self.send_as_json(payload), loop=self.loop)
93 | +
94 | async def received_message(self, msg):
95 | self._dispatch('socket_raw_receive', msg)
96 |
97 | --
98 | 2.30.2
99 |
100 |
--------------------------------------------------------------------------------
/patches/html2pango/0001-allow-usage-from-python.patch:
--------------------------------------------------------------------------------
1 | From 08945b9300fee9611b5619eb8ea9de82d1ae0471 Mon Sep 17 00:00:00 2001
2 | From: Raidro Manchester
3 | Date: Sun, 4 Jul 2021 18:10:39 +0300
4 | Subject: [PATCH] allow usage from python
5 |
6 | It fits my usecase to use this library, except from python. Even though
7 | I barely know Rust at all, here it works with PyO3.
8 | ---
9 | Cargo.toml | 8 ++++++++
10 | src/lib.rs | 18 ++++++++++++++++++
11 | 2 files changed, 26 insertions(+)
12 |
13 | diff --git a/Cargo.toml b/Cargo.toml
14 | index a703ad3..07c240d 100644
15 | --- a/Cargo.toml
16 | +++ b/Cargo.toml
17 | @@ -14,6 +14,10 @@ documentation = "https://world.pages.gitlab.gnome.org/html2pango/html2pango/"
18 | repository = "https://gitlab.gnome.org/World/html2pango"
19 | edition = "2018"
20 |
21 | +[lib]
22 | +name = "html2pango"
23 | +crate-type = ["cdylib"]
24 | +
25 | [dependencies]
26 | regex = "1.4.2"
27 | lazy_static = "1.4.0"
28 | @@ -24,5 +28,9 @@ anyhow = "1.0.35"
29 | html5ever = "0.25.1"
30 | markup5ever_rcdom = "0.1.0"
31 |
32 | +[dependencies.pyo3]
33 | +version = "0.14.0"
34 | +features = ["extension-module"]
35 | +
36 | [dev-dependencies]
37 | pretty_assertions = "0.6.1"
38 | diff --git a/src/lib.rs b/src/lib.rs
39 | index b583e35..08ed289 100644
40 | --- a/src/lib.rs
41 | +++ b/src/lib.rs
42 | @@ -11,12 +11,25 @@ use maplit::{hashmap, hashset};
43 |
44 | use regex::Regex;
45 |
46 | +use pyo3::prelude::*;
47 | +
48 | const AMP: &str = "(&)";
49 | const DOMAIN: &str = "[^\\s,)(\"]+";
50 | const HASH: &str = "(#[\\w._-]+)?";
51 |
52 | pub mod block;
53 |
54 | +#[pymodule]
55 | +fn html2pango(_py: Python, m: &PyModule) -> PyResult<()> {
56 | + m.add_function(wrap_pyfunction!(markup, m)?)?;
57 | + m.add_function(wrap_pyfunction!(markup_from_raw, m)?)?;
58 | + m.add_function(wrap_pyfunction!(matrix_html_to_markup, m)?)?;
59 | + m.add_function(wrap_pyfunction!(html_escape, m)?)?;
60 | + m.add_function(wrap_pyfunction!(markup_links, m)?)?;
61 | +
62 | + Ok(())
63 | +}
64 | +
65 | /// Sanitize the input using [`ammonia`][ammonia]'s defaults,
66 | /// Convert the input `&str` to pango format and parse
67 | /// URLS to show as `pango` markup links(removes rel attributes).
68 | @@ -51,6 +64,7 @@ pub mod block;
69 | /// ```
70 | ///
71 | /// [ammonia]: https://docs.rs/ammonia/1.1.0/ammonia/fn.clean.html
72 | +#[pyfunction]
73 | pub fn markup(s: &str) -> String {
74 | let sanitized_html = ammonia::Builder::new().link_rel(None).clean(s).to_string();
75 | markup_from_raw(&sanitized_html)
76 | @@ -75,6 +89,7 @@ pub fn markup(s: &str) -> String {
77 | /// let m = markup_from_raw("with links: http://gnome.org");
78 | /// assert_eq!(&m, "with links: http://gnome.org");
79 | /// ```
80 | +#[pyfunction]
81 | pub fn markup_from_raw(s: &str) -> String {
82 | lazy_static! {
83 | static ref PARAM: String = format!("({amp}?\\w+(=[\\w._-]+)?)", amp = AMP);
84 | @@ -123,6 +138,7 @@ pub fn markup_from_raw(s: &str) -> String {
85 | }
86 |
87 | // WIP: only allow the html subset that matrix uses.
88 | +#[pyfunction]
89 | pub fn matrix_html_to_markup(s: &str) -> String {
90 | // https://github.com/matrix-org/matrix-react-sdk/blob/4bf5e44b2043bbe95faa66943878acad23dfb823/src/HtmlUtils.js#L178-L184
91 | #[rustfmt::skip]
92 | @@ -164,6 +180,7 @@ pub fn matrix_html_to_markup(s: &str) -> String {
93 | }
94 |
95 | /// Escape the html entities of `s`
96 | +#[pyfunction]
97 | pub fn html_escape(s: &str) -> String {
98 | s.to_string()
99 | .replace('&', "&")
100 | @@ -173,6 +190,7 @@ pub fn html_escape(s: &str) -> String {
101 | }
102 |
103 | /// Converts links to LINK
104 | +#[pyfunction]
105 | pub fn markup_links(s: &str) -> String {
106 | let mut parsed = String::with_capacity(s.len());
107 | let finder = LinkFinder::new();
108 | --
109 | 2.30.2
110 |
111 |
--------------------------------------------------------------------------------
/patches/libadwaita/0001-revert-stylesheet-softer-window-shadows.patch:
--------------------------------------------------------------------------------
1 | From 31dc61d91d3602af18ca8c48a532479a26a3b73c Mon Sep 17 00:00:00 2001
2 | From: Raidro Manchester
3 | Date: Sat, 28 Aug 2021 10:35:52 +0300
4 | Subject: [PATCH] Revert "stylesheet: Softer window shadows"
5 |
6 | It makes it barely usable on X11 as the default sizes are completely
7 | wrong.
8 | This reverts commit 1ca63211d69b7d44401efccd62409c81a27f86eb.
9 | ---
10 | src/stylesheet/widgets/_window.scss | 12 ++++--------
11 | 1 file changed, 4 insertions(+), 8 deletions(-)
12 |
13 | diff --git a/src/stylesheet/widgets/_window.scss b/src/stylesheet/widgets/_window.scss
14 | index e447e18..01ec0a8 100644
15 | --- a/src/stylesheet/widgets/_window.scss
16 | +++ b/src/stylesheet/widgets/_window.scss
17 | @@ -8,10 +8,8 @@ window {
18 | $_wm_border_backdrop: if($variant=='light', transparentize(black, 0.82), transparentize(black, 0.25));
19 |
20 | &.csd {
21 | - box-shadow: 0 1px 3px 3px transparent,
22 | - 0 2px 8px 2px transparentize(black, 0.87),
23 | - 0 3px 20px 10px transparentize(black, 0.91),
24 | - 0 6px 32px 16px transparentize(black, 0.96),
25 | + box-shadow: 0 3px 9px 1px transparentize(black, 0.5),
26 | + 0 2px 6px 2px transparent,
27 | 0 0 0 1px $_wm_border; //doing borders with box-shadow
28 | margin: 0px;
29 | border-radius: $window_radius;
30 | @@ -27,10 +25,8 @@ window {
31 | // change when we go to backdrop, to prevent jumping windows.
32 | // The biggest shadow should be in the same order then in the active state
33 | // or the jumping will happen during the transition.
34 | - box-shadow: 0 1px 3px 3px transparentize(black, 0.91),
35 | - 0 2px 14px 5px transparentize(black, 0.95),
36 | - 0 4px 28px 12px transparentize(black, 0.97),
37 | - 0 6px 32px 16px transparent,
38 | + box-shadow: 0 3px 9px 1px transparent,
39 | + 0 2px 6px 2px transparentize(black, 0.8),
40 | 0 0 0 1px $_wm_border_backdrop;
41 | transition: $backdrop_transition;
42 | }
43 | --
44 | 2.31.1
45 |
46 |
--------------------------------------------------------------------------------
/po/LINGUAS:
--------------------------------------------------------------------------------
1 | lt
2 |
--------------------------------------------------------------------------------
/po/POTFILES.in:
--------------------------------------------------------------------------------
1 | data/org.gnome.gitlab.ranchester.Mirdorph.desktop.in
2 | data/ui/about_dialog.ui.in
3 | data/ui/channel_inner_window.ui
4 | data/ui/channel_properties_window.ui
5 | data/ui/channel_sidebar.ui
6 | data/ui/context_error_dialog.ui
7 | data/ui/image_viewer.ui
8 | data/ui/login_window.ui
9 | data/ui/main_window.ui
10 | data/ui/settings_window.ui
11 | data/ui/tos_notice.ui
12 | data/ui/login_window.ui
13 | src/attachment.py
14 | src/discord_web_grabber.py
15 | src/message_entry_bar.py
16 | src/typing_indicator.py
17 |
--------------------------------------------------------------------------------
/po/README.md:
--------------------------------------------------------------------------------
1 | # How to create and update a translation
2 |
3 | First, run the script `update_potfiles.sh` like this, where `LANGUAGE` is the language code that you want to add or update (`it`: italian, `fr`: french, `es`: spanish...):
4 |
5 | ```bash
6 | cd po
7 | ./update_potfiles.sh LANGUAGE
8 | ```
9 |
10 | It will ask for an email, provide yours if you want and it will be used to credit you. It's historically been used to report issues in the translation for a specific language, but nowadays with issue systems and easier bug reporting than ever it's not really necessary.
11 |
12 | Finally edit the `.po` file that was just created with the language code you used. You can use a normal text editor or a simpler tool like **lokalize** or **poedit** (you can probably find both in your distribution's repositories).
13 |
14 | Note that it also seems you need to update LINGUAS (and make sure in abc order).
15 |
16 | Attribution: I copied and adapted this translation code from GIARA by Gabmus, who himself copied it from uberwriter.
17 |
--------------------------------------------------------------------------------
/po/meson.build:
--------------------------------------------------------------------------------
1 | i18n.gettext(meson.project_name(),
2 | preset: 'glib'
3 | )
4 |
--------------------------------------------------------------------------------
/po/update_potfiles.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | APPNAME="mirdorph"
4 |
5 | if [ -z $1 ]; then
6 | echo "Usage: $0 lang"
7 | exit
8 | fi
9 | lang="$1"
10 |
11 | rm *.pot
12 |
13 | version=$(fgrep -m 1 "version: " ../meson.build | grep -v "meson" | grep -o "'.*'" | sed "s/'//g")
14 |
15 | find ../src -iname "*.py" | xargs xgettext --package-name=$APPNAME --package-version=$version --from-code=UTF-8 --output=$APPNAME-python.pot
16 | find ../data/ui ../src/plugins -iname "*.glade" -or -iname "*.xml" -or -iname "*.ui" -or -iname "*.ui.in" | xargs xgettext --package-name=$APPNAME --package-version=$version --from-code=UTF-8 --output=$APPNAME-glade.pot -L Glade
17 | find ../data/ -iname "*.desktop.in" | xargs xgettext --package-name=$APPNAME --package-version=$version --from-code=UTF-8 --output=$APPNAME-desktop.pot -L Desktop
18 |
19 | msgcat --use-first $APPNAME-python.pot $APPNAME-glade.pot $APPNAME-desktop.pot > $APPNAME.pot
20 |
21 | sed 's/#: //g;s/:[0-9]*//g;s/\.\.\///g' <(fgrep "#: " $APPNAME.pot) | sort | uniq | sed 's/ /\n/g' | uniq > POTFILES.in
22 |
23 | [ -f "${lang}.po" ] && mv "${lang}.po" "${lang}.po.old"
24 | msginit --locale=$lang --input $APPNAME.pot
25 | if [ -f "${lang}.po.old" ]; then
26 | mv "${lang}.po" "${lang}.po.new"
27 | msgmerge -N "${lang}.po.old" "${lang}.po.new" > ${lang}.po
28 | rm "${lang}.po.old" "${lang}.po.new"
29 | fi
30 | sed -i 's/ASCII/UTF-8/' "${lang}.po"
31 | rm *.pot
32 |
--------------------------------------------------------------------------------
/tests/load_gtk.py:
--------------------------------------------------------------------------------
1 | # Loading the Gresource for importing modules with composite templates, and stuff like
2 | # Adw.init + version requires
3 | import os
4 | import gi
5 | gi.require_version("Gtk", "4.0")
6 | gi.require_version("Adw", "1")
7 | from gi.repository import Gio
8 | # About dialog is created by meson which we can't use
9 | os.system("sed '/about_dialog/d' ../data/mirdorph.gresource.xml > ../data/unmeson-mirdorph.gresource.xml")
10 | os.system("cd ../data/ && glib-compile-resources unmeson-mirdorph.gresource.xml")
11 | resource = Gio.resource_load("../data/unmeson-mirdorph.gresource")
12 | Gio.Resource._register(resource)
13 |
14 | from gi.repository import Adw
15 | Adw.init()
16 |
--------------------------------------------------------------------------------
/tests/test_confman.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import pytest
3 | # Workaround from stackoverflow to allow importing the program
4 | sys.path.append("..")
5 |
6 | from mirdorph.confman import ConfManager
7 |
8 | @pytest.fixture()
9 | def confman(tmp_path):
10 | conf_file_path = tmp_path / "conf"
11 | yield ConfManager(conf_file_path)
12 |
13 | class TestConfMan:
14 | def test_create(self, confman):
15 | assert confman is not None
16 |
17 | def test_base_schema_copied(self, confman):
18 | assert confman.BASE_SCHEMA["example"] == confman.get_value("example")
19 |
20 | def test_setting_value(self, confman):
21 | ex_val = True
22 | confman.set_value("test", ex_val)
23 | assert confman.get_value("test") == ex_val
24 |
25 | def test_persistence(self, tmp_path):
26 | # Manually
27 | conf_file_path = tmp_path / "conf"
28 | confman = ConfManager(conf_file_path)
29 |
30 | ex_val = 12
31 | confman.set_value("test", 12)
32 |
33 | del(confman)
34 | new_confman = ConfManager(conf_file_path)
35 |
36 | assert new_confman.get_value("test") == ex_val
37 |
38 |
--------------------------------------------------------------------------------
/tests/test_message_parsing.py:
--------------------------------------------------------------------------------
1 | import sys
2 | # Workaround from stackoverflow to allow importing the program
3 | sys.path.append("..")
4 |
5 | # Message parsing has gresource templates, and linkpreview uses handy
6 | import tests.load_gtk
7 | from mirdorph.link_preview import LinkPreviewExport
8 | from mirdorph.message_parsing import _create_pango_markup, calculate_msg_parts, _generate_exports, ComponentType
9 |
10 | def test_create_pango_markup_links():
11 | test_text = """\
12 | with a link https://google.com
13 | and other http://example.com"""
14 | expected_text = """\
15 | with a link https://google.com
16 | and other http://example.com"""
17 |
18 | assert _create_pango_markup(test_text) == expected_text
19 |
20 | def test_create_pango_markup_escaping():
21 | test_text = """\
22 | spooky non escaped
23 | other"""
24 | expected_text = """\
25 | spooky <b>non escaped</b>
26 | <a href="hello">other</a>"""
27 |
28 | assert _create_pango_markup(test_text) == expected_text
29 |
30 | # Why not test all of markdown? Because html2pango has tests in itself.
31 |
32 | def test_calculate_message_parts():
33 | example_message = """\
34 | Hello, this is a test message
35 | > That has a quote
36 | > And it continues
37 | > Almost forever
38 | Until it suddenly stops and you don't
39 | know what to do.
40 | However the bot said that:
41 | >>> No this doesn't make any sense."""
42 |
43 | correct_components = [
44 | (
45 | ComponentType.STANDARD,
46 | "Hello, this is a test message"
47 | ),
48 | (
49 | ComponentType.QUOTE,
50 | "That has a quote\nAnd it continues\nAlmost forever"
51 | ),
52 | (
53 | ComponentType.STANDARD,
54 | "Until it suddenly stops and you don't\nknow what to do.\nHowever the bot said that:"
55 | ),
56 | (
57 | ComponentType.QUOTE,
58 | "No this doesn't make any sense."
59 | )
60 | ]
61 |
62 | components = calculate_msg_parts(example_message)
63 |
64 | assert correct_components == components
65 |
66 |
--------------------------------------------------------------------------------
/tests/test_plug_engine.py:
--------------------------------------------------------------------------------
1 | import load_gtk
2 | import sys
3 | import pytest
4 | import gi
5 | # Workaround from stackoverflow to allow importing the program
6 | sys.path.append("..")
7 |
8 | from gi.repository import Gtk, Gio
9 | from mirdorph.plugin import MrdPluginEngine, MrdExtensionSet, MrdPlugin
10 |
11 |
12 | def test_plugin_discovery():
13 | engine = MrdPluginEngine()
14 | # We currently have atleast an example plugin
15 | plugins = engine.get_available_plugins()
16 | assert plugins
17 |
18 |
19 | def test_plugin_load_unload():
20 | engine = MrdPluginEngine()
21 | for av_plugin in engine.get_available_plugins():
22 | # A plugin that is built-in won't be deactivated at first,
23 | # so we should ensure that.
24 | # and a for plugin that is (which we expect to exit), we can
25 | # test this here.
26 | if av_plugin.built_in:
27 | assert av_plugin.active
28 | av_plugin.active = False
29 | else:
30 | plugin = av_plugin
31 |
32 | assert not plugin.active
33 | assert not engine.get_enabled_plugins()
34 | engine.load_plugin(plugin)
35 | assert plugin.active
36 | assert engine.get_enabled_plugins()
37 | engine.unload_plugin(plugin)
38 | assert not plugin.active
39 | assert not engine.get_enabled_plugins()
40 |
41 |
42 | def test_is_plugin_compatible():
43 | engine = MrdPluginEngine()
44 | plugin = engine.get_available_plugins()[0]
45 | engine.load_plugin(engine.get_available_plugins()[0])
46 |
47 | extension_set = MrdExtensionSet(
48 | engine,
49 | # Will be correct
50 | plugin.type
51 | )
52 | assert engine.is_plugin_compatible(plugin, extension_set)
53 |
54 | class FakePlugin(MrdPlugin):
55 | pass
56 | extension_set = MrdExtensionSet(
57 | engine,
58 | # Incompatible
59 | FakePlugin
60 | )
61 | assert not engine.is_plugin_compatible(plugin, extension_set)
62 |
63 | def test_get_plugin_by_module():
64 | engine = MrdPluginEngine()
65 | plugin = engine.get_available_plugins()[0]
66 |
67 | plugin_by_mod_name = engine.get_plugin_from_module(plugin.module_name)
68 |
69 | assert plugin is plugin_by_mod_name
70 |
71 | def test_extension_set_discovery():
72 | engine = MrdPluginEngine()
73 | plugin = engine.get_available_plugins()[0]
74 | engine.load_plugin(plugin)
75 |
76 | extension_set = MrdExtensionSet(
77 | engine,
78 | # Any plugin is ok
79 | plugin.type
80 | )
81 | assert list(extension_set)
82 |
83 | class FakePlugin(MrdPlugin):
84 | pass
85 | extension_set_wrong = MrdExtensionSet(
86 | engine,
87 | FakePlugin
88 | )
89 | assert not list(extension_set_wrong)
90 |
91 |
92 | def test_extension_set_signals(mocker):
93 | engine = MrdPluginEngine()
94 | plugin = engine.get_available_plugins()[0]
95 | plugin.active = False
96 | extension_set = MrdExtensionSet(
97 | engine,
98 | plugin.type
99 | )
100 |
101 | mock = mocker.patch.object(extension_set, "emit")
102 | engine.load_plugin(plugin)
103 | mock.assert_called_with("extension_added", plugin)
104 | engine.unload_plugin(plugin)
105 | mock.assert_called_with("extension_removed", plugin)
106 |
107 | def test_extension_state_change_via_property(mocker):
108 | # Signals are best as a showcase of the affects of enabling,
109 | # simply setting the active property (useful for binding) should
110 | # also do this.
111 | engine = MrdPluginEngine()
112 | plugin = engine.get_available_plugins()[0]
113 | plugin.active = False
114 | extension_set = MrdExtensionSet(
115 | engine,
116 | plugin.type
117 | )
118 |
119 | mock = mocker.patch.object(extension_set, "emit")
120 | plugin.active = True
121 | mock.assert_called_with("extension_added", plugin)
122 | plugin.active = False
123 | mock.assert_called_with("extension_removed", plugin)
124 |
125 | def test_gresource(mocker):
126 | engine = MrdPluginEngine()
127 |
128 | # We don't need to load the plugin as gresources are created on initial startup.
129 | # At first I thought about doing it on load and undoing on unload, however then implementations
130 | # can't really define anything that uses gresources.
131 | # Helloworld example - "welcome_message.txt"
132 | assert Gio.resources_get_info("/org/gnome/gitlab/ranchester/Mirdorph/plugins/helloworld/welcome_message.txt", 0)
133 |
--------------------------------------------------------------------------------