├── .env.sample
├── .github
└── workflows
│ └── docker-image.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── .gitignore
├── .idea
│ ├── .gitignore
│ ├── codeStyles
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── libraries-with-intellij-classes.xml
│ ├── misc.xml
│ ├── uiDesigner.xml
│ └── vcs.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── com
│ └── github
│ └── livingwithhippos
│ └── unchained_bot
│ ├── BotApplication.kt
│ ├── Constants.kt
│ ├── Main.kt
│ ├── data
│ ├── model
│ │ ├── APIError.kt
│ │ ├── Authentication.kt
│ │ ├── Credentials.kt
│ │ ├── DownloadItem.kt
│ │ ├── EmptyBodyInterceptor.kt
│ │ ├── Host.kt
│ │ ├── NetworkResponse.kt
│ │ ├── Stream.kt
│ │ ├── TorrentItem.kt
│ │ └── User.kt
│ ├── remote
│ │ ├── AuthApiHelper.kt
│ │ ├── AuthApiHelperImpl.kt
│ │ ├── AuthenticationApi.kt
│ │ ├── DownloadApiHelper.kt
│ │ ├── DownloadApiHelperImpl.kt
│ │ ├── DownloadsApi.kt
│ │ ├── HostsApi.kt
│ │ ├── HostsApiHelper.kt
│ │ ├── HostsApiHelperImpl.kt
│ │ ├── StreamingApi.kt
│ │ ├── StreamingApiHelper.kt
│ │ ├── StreamingApiHelperImpl.kt
│ │ ├── TorrentApiHelper.kt
│ │ ├── TorrentApiHelperImpl.kt
│ │ ├── TorrentsApi.kt
│ │ ├── UnrestrictApi.kt
│ │ ├── UnrestrictApiHelper.kt
│ │ ├── UnrestrictApiHelperImpl.kt
│ │ ├── UserApi.kt
│ │ ├── UserApiHelper.kt
│ │ ├── UserApiHelperImpl.kt
│ │ ├── VariousApi.kt
│ │ ├── VariousApiHelper.kt
│ │ └── VariousApiHelperImpl.kt
│ └── repository
│ │ ├── AuthenticationRepository.kt
│ │ ├── BaseRepository.kt
│ │ ├── CredentialsRepository.kt
│ │ ├── DownloadRepository.kt
│ │ ├── StreamingRepository.kt
│ │ ├── TorrentsRepository.kt
│ │ ├── UnrestrictRepository.kt
│ │ ├── UserRepository.kt
│ │ └── VariousApiRepository.kt
│ ├── di
│ ├── ApiFactory.kt
│ ├── KoinModule.kt
│ └── MoshiAdapter.kt
│ ├── localization
│ ├── EN.kt
│ ├── IT.kt
│ └── Localization.kt
│ └── utilities
│ ├── Extensions.kt
│ └── Patterns.kt
└── docker-compose.yml
/.env.sample:
--------------------------------------------------------------------------------
1 | # Do NOT commit this file on a public repo/fork when you replace these variables.
2 | # If you really want to commit it, create a new PRIVATE repository, copy and paste my code onto it and use that.
3 | BOT_TOKEN=get one from https://core.telegram.org/bots#3-how-do-i-create-a-bot
4 | API_KEY=get one from https://real-debrid.com/apitoken
--------------------------------------------------------------------------------
/.github/workflows/docker-image.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker image
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | push_to_registry:
9 | name: Push Docker image to Docker Hub
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check out the repo
13 | uses: actions/checkout@v2
14 |
15 | - name: Extract metadata for Docker
16 | id: meta
17 | uses: docker/metadata-action@v3
18 | with:
19 | images: livingwithhippos/unchainedbotkotlin
20 |
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v2
23 | with:
24 | platforms: arm64,amd64
25 |
26 | - name: Set up Docker Buildx
27 | id: buildx
28 | uses: docker/setup-buildx-action@v2
29 |
30 | - name: Log in to DockerHub
31 | uses: docker/login-action@v2
32 | with:
33 | username: ${{ secrets.DOCKERHUB_USERNAME }}
34 | password: ${{ secrets.DOCKER_PASSWORD }}
35 |
36 | - name: Build and push Docker image
37 | uses: docker/build-push-action@v3
38 | with:
39 | context: .
40 | push: true
41 | tags: ${{ steps.meta.outputs.tags }}
42 | labels: ${{ steps.meta.outputs.labels }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | app/build/
3 |
4 | # Ignore Gradle GUI config
5 | gradle-app.setting
6 |
7 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
8 | !gradle-wrapper.jar
9 |
10 | # Cache of project
11 | .gradletasknamecache
12 |
13 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
14 | # gradle/wrapper/gradle-wrapper.properties
15 | app/credentials.json
16 | .env
17 | app/wget-log
18 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gradle:7-jdk AS build
2 |
3 | # set version label
4 | ARG BUILD_DATE
5 | ARG VERSION
6 | LABEL build_version="LivingWithHippos version:- ${VERSION} Build-date:- ${BUILD_DATE}"
7 | LABEL maintainer="LivingWithHippos"
8 |
9 | COPY --chown=gradle:gradle app /home/gradle/src
10 | WORKDIR /home/gradle/src
11 | RUN ./gradlew Jar
12 |
13 | FROM azul/zulu-openjdk-alpine:18-jre
14 |
15 | RUN \
16 | echo "**** install runtime packages ****" && \
17 | apk add --no-cache wget
18 |
19 | RUN mkdir /app
20 | COPY --from=build /home/gradle/src/build/libs/ /app/
21 | # downloaded files will end up here
22 | VOLUME /downloads
23 |
24 | ENTRYPOINT ["java","-jar","/app/unchained-bot-kotlin.jar"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # unchained-bot-kotlin
2 |
3 | Telegram Bot written in Kotlin for Real-Debrid.
4 |
5 | Unchained Bot Kotlin is a [Telegram Bot](https://core.telegram.org/bots) that allows you to interface with [Real-Debrid](https://real-debrid.com/). If you want to support me, you can instead click through [this referral link](http://real-debrid.com/?id=78841).
6 |
7 | My [previous bot](https://github.com/LivingWithHippos/unchained-bot) was written in Python, but I realized that since [Unchained for Android](https://github.com/LivingWithHippos/unchained-android) was much more completed and a [telegram library for Kotlin](https://github.com/kotlin-telegram-bot/kotlin-telegram-bot) was available I could port the application with minimal effort. In fact around 90% of the code is shared with Unchained for Android.
8 |
9 | - [unchained-bot-kotlin](#unchained-bot-kotlin)
10 | * [Installation](#installation)
11 | + [Docker (recommended)](#docker--recommended-)
12 | - [Docker compose](#docker-compose)
13 | - [Docker cli](#docker-cli)
14 | + [As a Kotlin/java application](#as-a-kotlin-java-application)
15 | + [Parameters](#parameters)
16 | * [Available Commands](#available-commands)
17 |
18 | ## Installation
19 |
20 | ### Docker (recommended)
21 |
22 | An official image is available on [dockerhub](https://hub.docker.com/r/livingwithhippos/unchainedbotkotlin). It is also possible to clone the repository and build an image from the Dockerfile with `docker build -t livingwithhippos/unchainedbot:0.1 .` and run it with either docker cli o docker compose.
23 |
24 | #### Docker compose
25 |
26 | ```yaml
27 | version: '3'
28 |
29 | services:
30 | unchained:
31 | image: livingwithhippos/unchainedbotkotlin:latest
32 | container_name: unchainedbot
33 | restart: unless-stopped
34 | environment:
35 | - TELEGRAM_BOT_TOKEN=${BOT_TOKEN}
36 | - PRIVATE_API_KEY=${API_KEY}
37 | # optional
38 | # only let this user use the bot
39 | # - WHITELISTED_USER=your telegram user id
40 | # add arguments for wget, userd for /get
41 | # - WGET_ARGUMENTS=see https://www.gnu.org/software/wget/manual/wget.html, default is "--no-verbose"
42 | # OkHttp log level
43 | # - LOG_LEVEL=availabel options are error, body, basic, headers, none. Default is error
44 | volumes:
45 | - ./downloads:/downloads
46 | ```
47 |
48 | Copy or move the `.env.sample` file to `.env` and add the necessary arguments.
49 |
50 | #### Docker cli
51 |
52 | ```shell
53 | docker run -d \
54 | --name=unchainedbot \
55 | -e TELEGRAM_BOT_TOKEN=abc `#required` \
56 | -e PRIVATE_API_KEY=def `#required` \
57 | -e WHITELISTED_USER= `#optional` \
58 | -e WGET_ARGUMENTS= `#optional` \
59 | -e LOG_LEVEL= `#optional` \
60 | -v ./downloads:/downloads \
61 | --restart unless-stopped \
62 | livingwithhippos/unchainedbotkotlin:latest
63 | ```
64 |
65 | #### Other optional volumes
66 |
67 | It is possible to mount the log file for wget and the config file for wget.
68 |
69 |
70 | ### As a java application
71 |
72 | After cloning the project, navigate to the `app` folder and run `./gradlew Jar`. This will generate the file `unchained-bot-kotlin/app/build/libs/unchained-bot-kotlin.jar`. You can also download the `unchained-bot-kotlin.jar` file from the release page.
73 |
74 | You can then move the file somewhere and run the bot with
75 |
76 | ```shell
77 | java \
78 | -DTELEGRAM_BOT_TOKEN=your-token \
79 | -DPRIVATE_API_KEY=your-key \
80 | -DDOWNLOADS_PATH=your-download-folder \
81 | -jar unchained-bot-kotlin.jar
82 | ```
83 |
84 | To use the `/get` command you need wget installed.
85 |
86 | If you don't use docker or already have java installed, this file is just ~ 9 MB. The docker image is around 230 MB.
87 |
88 | ### Parameters
89 |
90 | | Parameter | Function |
91 | |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
92 | | TELEGRAM_BOT_TOKEN | Required. Get your telegram token from https://core.telegram.org/bots#3-how-do-i-create-a-bot |
93 | | PRIVATE_API_KEY | Required. Get your private API key from https://real-debrid.com/apitoken |
94 | | WHITELISTED_USER | let only this user utilize the bot. Needs the user's telegram ID. |
95 | | ENABLE_QUERIES | Lets you mention the bot with a `@botName link` syntax from any chat to unrestrict a link. NOT RESTRICTED TO WHITELISTED USER!!! Values "true"or "false" (default) |
96 | | LOCALE | Set the current translation. The values "en" for english and "it" for italian are available |
97 | | WGET_ARGUMENTS | wget is used to download files locally. Pass arguments to it with this |
98 | | LOG_LEVEL | default is error, if you have issues you can set this to another level like body, basic, headers, none (currently bugged, do not use) |
99 | | TEMP_PATH | path where temporary files, such are `.torrent` files, are being downloaded. You probably won't change this. |
100 | | DOWNLOADS_PATH | the folder where files are downloaded with `/get`. If you're using docker just change the mounted folder instead: `/new/path:/downloads` |
101 |
102 | ## Available Commands
103 |
104 | Parameters between [square brackets] are optional.
105 |
106 | | Parameter | Function |
107 | |--------------------------------|---|
108 | | /help | display the list of available commands |
109 | | /user | get Real Debrid user's information |
110 | | /torrents [number, default 5] | list the last torrents |
111 | | /downloads [number, default 5] | list the last downloads |
112 | | /get unrestricted_link | downloads the link on the directory of the server running the bot |
113 | | /unrestrict url |magnet|torrent file link | generate a download link. Magnet/Torrents will be queued, check their status with /torrents |
114 | | /transcode real_debrid_file_id | transcode streaming links to various quality levels. Get the file id using /unrestrict |
115 |
116 | ## Thanks, Mr. Unchained
117 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/shelf
3 | /confluence/target
4 | /dependencies/repo
5 | /android.tests.dependencies
6 | /dependencies/android.tests.dependencies
7 | /dist
8 | /local
9 | /gh-pages
10 | /ideaSDK
11 | /clionSDK
12 | /android-studio/sdk
13 | out/
14 | /tmp
15 | workspace.xml
16 | *.versionsBackup
17 | /idea/testData/debugger/tinyApp/classes*
18 | /jps-plugin/testData/kannotator
19 | /js/js.translator/testData/out/
20 | /js/js.translator/testData/out-min/
21 | /js/js.translator/testData/out-pir/
22 | .gradle/
23 | build/
24 | !**/src/**/build
25 | !**/test/**/build
26 | *.iml
27 | !**/testData/**/*.iml
28 | .idea/libraries/Gradle*.xml
29 | .idea/libraries/Maven*.xml
30 | .idea/artifacts/PILL_*.xml
31 | .idea/artifacts/KotlinPlugin.xml
32 | .idea/modules
33 | .idea/runConfigurations/JPS_*.xml
34 | .idea/runConfigurations/PILL_*.xml
35 | .idea/libraries
36 | .idea/modules.xml
37 | .idea/gradle.xml
38 | .idea/compiler.xml
39 | .idea/inspectionProfiles/profiles_settings.xml
40 | .idea/.name
41 | .idea/artifacts/dist_auto_*
42 | .idea/artifacts/dist.xml
43 | .idea/artifacts/ideaPlugin.xml
44 | .idea/artifacts/kotlinc.xml
45 | .idea/artifacts/kotlin_compiler_jar.xml
46 | .idea/artifacts/kotlin_plugin_jar.xml
47 | .idea/artifacts/kotlin_jps_plugin_jar.xml
48 | .idea/artifacts/kotlin_daemon_client_jar.xml
49 | .idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml
50 | .idea/artifacts/kotlin_main_kts_jar.xml
51 | .idea/artifacts/kotlin_compiler_client_embeddable_jar.xml
52 | .idea/artifacts/kotlin_reflect_jar.xml
53 | .idea/artifacts/kotlin_stdlib_js_ir_*
54 | .idea/artifacts/kotlin_test_js_ir_*
55 | .idea/artifacts/kotlin_stdlib_wasm_*
56 | .idea/jarRepositories.xml
57 | kotlin-ultimate/
58 | node_modules/
59 | .rpt2_cache/
60 | libraries/tools/kotlin-test-js-runner/lib/
61 | local.properties
62 | buildSrcTmp/
63 | distTmp/
64 | outTmp/
--------------------------------------------------------------------------------
/app/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/app/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/.idea/libraries-with-intellij-classes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
64 |
65 |
--------------------------------------------------------------------------------
/app/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/.idea/uiDesigner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/app/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 |
3 | plugins {
4 | kotlin("jvm") version "1.6.10"
5 | kotlin("kapt") version "1.6.10"
6 | application
7 | id("com.github.ben-manes.versions") version "0.42.0"
8 | }
9 |
10 | group = "com.github.livingwithhippos"
11 | version = "0.6.3"
12 |
13 | repositories {
14 | mavenCentral()
15 | maven { url = uri("https://jitpack.io") }
16 | }
17 |
18 | val ktlint: Configuration by configurations.creating
19 |
20 | dependencies {
21 |
22 | val kotlinVersion = "1.6.10"
23 | val coroutinesVersion = "1.6.2"
24 | val telegramVersion = "6.0.7"
25 | val moshiVersion = "1.13.0"
26 | val retrofitVersion = "2.9.0"
27 | val okhttpVersion = "4.9.3"
28 | val koinVersion = "3.2.0"
29 | val ktLintVersion = "0.45.2"
30 |
31 | // kotlin stdlib
32 | implementation ("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
33 | implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
34 |
35 | // coroutines
36 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
37 |
38 | // Koin for Kotlin
39 | implementation ("io.insert-koin:koin-core:$koinVersion")
40 |
41 | // telegram bot
42 | implementation ("io.github.kotlin-telegram-bot.kotlin-telegram-bot:telegram:$telegramVersion")
43 |
44 | // moshi
45 | implementation ("com.squareup.moshi:moshi-kotlin:$moshiVersion")
46 | kapt ("com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion")
47 |
48 | // retrofit
49 | implementation ("com.squareup.retrofit2:retrofit:$retrofitVersion")
50 | implementation ("com.squareup.retrofit2:converter-moshi:$retrofitVersion")
51 |
52 | //okhttp
53 | implementation ("com.squareup.okhttp3:okhttp:$okhttpVersion")
54 | //okhttp logging. It's already used by the telegram bot library and can be set with Loglevel.Network
55 | implementation ("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
56 |
57 | ktlint("com.pinterest:ktlint:$ktLintVersion")
58 | }
59 |
60 | tasks {
61 |
62 | // creates a fat jar (with dependencies) for Docker, using ./gradlew Jar
63 | withType {
64 |
65 | manifest.attributes["Main-Class"] = "com.github.livingwithhippos.unchained_bot.MainKt"
66 | // remove the version from the Jar to make it easier to launch in the Dockerfile
67 | setProperty("archiveVersion","")
68 |
69 | configurations["compileClasspath"].forEach { file: File ->
70 | from(zipTree(file.absoluteFile))
71 | }
72 | }
73 | }
74 |
75 | // ktlint stuff
76 | val outputDir = "${project.buildDir}/reports/ktlint/"
77 | val inputFiles = project.fileTree(mapOf("dir" to "src", "include" to "**/*.kt"))
78 |
79 | val ktlintCheck by tasks.creating(JavaExec::class) {
80 | inputs.files(inputFiles)
81 | outputs.dir(outputDir)
82 |
83 | description = "Check Kotlin code style."
84 | classpath = ktlint
85 | main = "com.pinterest.ktlint.Main"
86 | args = listOf("src/**/*.kt")
87 | }
88 |
89 | val ktlintFormat by tasks.creating(JavaExec::class) {
90 | inputs.files(inputFiles)
91 | outputs.dir(outputDir)
92 |
93 | description = "Fix Kotlin code style deviations."
94 | classpath = ktlint
95 | main = "com.pinterest.ktlint.Main"
96 | args = listOf("-F", "src/**/*.kt")
97 | }
98 |
--------------------------------------------------------------------------------
/app/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/app/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LivingWithHippos/unchained-bot-kotlin/98623ed013f71dfa30050a4ce51f27fcf47ac53a/app/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/app/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "unchained-bot-kotlin"
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/BotApplication.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot
2 |
3 | import com.github.kotlintelegrambot.bot
4 | import com.github.kotlintelegrambot.dispatch
5 | import com.github.kotlintelegrambot.dispatcher.inlineQuery
6 | import com.github.kotlintelegrambot.dispatcher.message
7 | import com.github.kotlintelegrambot.entities.ChatId
8 | import com.github.kotlintelegrambot.entities.ParseMode
9 | import com.github.kotlintelegrambot.entities.inlinequeryresults.InlineQueryResult
10 | import com.github.kotlintelegrambot.entities.inlinequeryresults.InputMessageContent
11 | import com.github.kotlintelegrambot.extensions.filters.Filter
12 | import com.github.kotlintelegrambot.logging.LogLevel
13 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
14 | import com.github.livingwithhippos.unchained_bot.data.model.Stream
15 | import com.github.livingwithhippos.unchained_bot.data.model.TorrentItem
16 | import com.github.livingwithhippos.unchained_bot.data.model.UploadedTorrent
17 | import com.github.livingwithhippos.unchained_bot.data.model.User
18 | import com.github.livingwithhippos.unchained_bot.data.repository.DownloadRepository
19 | import com.github.livingwithhippos.unchained_bot.data.repository.StreamingRepository
20 | import com.github.livingwithhippos.unchained_bot.data.repository.TorrentsRepository
21 | import com.github.livingwithhippos.unchained_bot.data.repository.UnrestrictRepository
22 | import com.github.livingwithhippos.unchained_bot.data.repository.UserRepository
23 | import com.github.livingwithhippos.unchained_bot.localization.EN
24 | import com.github.livingwithhippos.unchained_bot.localization.Localization
25 | import com.github.livingwithhippos.unchained_bot.localization.localeMapping
26 | import com.github.livingwithhippos.unchained_bot.utilities.isMagnet
27 | import com.github.livingwithhippos.unchained_bot.utilities.isTorrent
28 | import com.github.livingwithhippos.unchained_bot.utilities.isWebUrl
29 | import kotlinx.coroutines.CoroutineScope
30 | import kotlinx.coroutines.Dispatchers
31 | import kotlinx.coroutines.Job
32 | import kotlinx.coroutines.delay
33 | import kotlinx.coroutines.launch
34 | import kotlinx.coroutines.withContext
35 | import okhttp3.OkHttpClient
36 | import okhttp3.Request
37 | import okio.buffer
38 | import okio.sink
39 | import org.koin.core.component.KoinComponent
40 | import org.koin.core.component.inject
41 | import java.io.BufferedReader
42 | import java.io.File
43 | import java.io.InputStreamReader
44 | import java.nio.charset.Charset
45 | import kotlin.system.exitProcess
46 |
47 | class BotApplication : KoinComponent {
48 |
49 | // Environment variables
50 | private val botToken: String = getKoin().getProperty("TELEGRAM_BOT_TOKEN") ?: ""
51 | private val privateApiKey: String = getKoin().getProperty("PRIVATE_API_KEY") ?: ""
52 | private val wgetArguments: String = getKoin().getProperty("WGET_ARGUMENTS") ?: "--no-verbose"
53 | private val logLevelArgument: String = getKoin().getProperty("LOG_LEVEL") ?: "error"
54 | private val enableQuery: String = getKoin().getProperty("ENABLE_QUERIES") ?: "false"
55 | private val enableQueriesArgument: Boolean = enableQuery.equals("true", true) || enableQuery == "1"
56 | private val whitelistedUser: Long = getKoin().getProperty("WHITELISTED_USER")?.toLongOrNull() ?: 0
57 | private val localeArgument: String = getKoin().getProperty("LOCALE") ?: "en"
58 |
59 | private val localization: Localization = localeMapping.getOrDefault(localeArgument, EN)
60 |
61 | // these are not useful for docker but for running it locally
62 | private val tempPath: String = getKoin().getProperty("TEMP_PATH") ?: "/tmp/"
63 | private val downloadsPath: String = getKoin().getProperty("DOWNLOADS_PATH") ?: "/downloads/"
64 |
65 | // repositories
66 | private val userRepository: UserRepository by inject()
67 | private val unrestrictRepository: UnrestrictRepository by inject()
68 | private val streamingRepository: StreamingRepository by inject()
69 | private val torrentsRepository: TorrentsRepository by inject()
70 | private val downloadRepository: DownloadRepository by inject()
71 |
72 | private val okHttpClient: OkHttpClient by inject()
73 |
74 | // coroutines
75 | private val job = Job()
76 | private val scope = CoroutineScope(Dispatchers.Default + job)
77 |
78 | private val download = Job()
79 | private val downloadScope = CoroutineScope(Dispatchers.IO + download)
80 |
81 | // Filters
82 | // Filter the whitelisted user, if any
83 | private val userFilter = if (whitelistedUser > 10000) Filter.User(whitelistedUser) else Filter.All
84 |
85 | // Command filters
86 | private val startCommandFilter = Filter.Custom { text?.startsWith("/start") ?: false }
87 | private val helpCommandFilter = Filter.Custom { text?.startsWith("/help") ?: false }
88 | private val userCommandFilter = Filter.Custom { text?.startsWith("/user") ?: false }
89 | private val unrestrictCommandFilter = Filter.Custom { text?.startsWith("/unrestrict") ?: false }
90 | private val transcodeCommandFilter = Filter.Custom { text?.startsWith("/transcode ") ?: false }
91 |
92 | private val getCommandFilter = Filter.Custom { (text?.startsWith("/get") ?: false) }
93 | private val torrentsCommandFilter = Filter.Custom { text?.startsWith("/torrents") ?: false }
94 | private val downloadsCommandFilter = Filter.Custom { (text?.startsWith("/downloads") ?: false) }
95 |
96 | fun startBot() {
97 |
98 | if (botToken.length > 40)
99 | println("Found Telegram Bot Token")
100 | else {
101 | println("[ERROR] Wrong or missing telegram bot token.\nCheck your telegram bot token with @BotFather and add it with the syntax TELEGRAM_BOT_TOKEN=...")
102 | exitProcess(1)
103 | }
104 |
105 | if (privateApiKey.length > 10)
106 | println("Found Real Debrid API Key")
107 | else {
108 | println("[ERROR] Wrong or missing real Real Debrid key.\nCheck your Real Debrid API Key and add it with the syntax PRIVATE_API_KEY=...")
109 | exitProcess(1)
110 | }
111 |
112 | printCurrentParameters()
113 |
114 | // check temp and downloads folder
115 | checkAndMakeDirectories(tempPath, downloadsPath)
116 |
117 | println("Starting bot...")
118 |
119 | val bot = bot {
120 |
121 | token = botToken
122 | timeout = 30
123 | // error when setting this, they need to update their OkHttp logging library
124 | logLevel = when (logLevelArgument) {
125 | "error" -> LogLevel.Error
126 | "body" -> LogLevel.Network.Body
127 | "basic" -> LogLevel.Network.Basic
128 | "headers" -> LogLevel.Network.Headers
129 | "none" -> LogLevel.Network.None
130 | else -> LogLevel.Error
131 | }
132 |
133 | // todo: add message splitting for when message length is > 4096
134 | dispatch {
135 |
136 | message(helpCommandFilter and userFilter) {
137 | bot.sendMessage(
138 | chatId = ChatId.fromId(message.chat.id),
139 | text = localization.helpMessage,
140 | parseMode = ParseMode.MARKDOWN
141 | )
142 | }
143 |
144 | message(startCommandFilter and userFilter) {
145 |
146 | bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = localization.botStarted)
147 |
148 | scope.launch {
149 | val user = getUser()
150 | if (user != null) {
151 |
152 | val welcome = localization.welcomeMessage.replace(
153 | "%username%", user.username
154 | ).replace(
155 | "%days%", (user.premium / 60 / 60 / 24).toString()
156 | ).replace(
157 | "%points%", user.points.toString()
158 | )
159 |
160 | bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = welcome)
161 | } else
162 | // close the bot?
163 | bot.sendMessage(
164 | chatId = ChatId.fromId(message.chat.id),
165 | text = localization.privateKeyError
166 | )
167 | }
168 | }
169 |
170 | message(userCommandFilter and userFilter) {
171 |
172 | scope.launch {
173 | val user = getUser()
174 | if (user != null) {
175 | val information = "${localization.username}: ${user.username}\n" +
176 | "${localization.status}: ${user.type}\n" +
177 | "${localization.email}: ${user.email}\n" +
178 | "${localization.points}: ${user.points}\n" +
179 | "${localization.premium}: ${user.premium / 60 / 60 / 24} ${localization.days}\n" +
180 | "${localization.expiration}: ${user.expiration}" +
181 | "${localization.id}: ${user.id}\n" +
182 | "${user.avatar} \n"
183 |
184 | bot.sendMessage(chatId = ChatId.fromId(message.chat.id), text = information)
185 | } else
186 | bot.sendMessage(
187 | chatId = ChatId.fromId(message.chat.id),
188 | text = localization.privateKeyError
189 | )
190 | }
191 | }
192 |
193 | message(unrestrictCommandFilter and userFilter) {
194 | val args = getArgAsString(message.text)
195 | if (!args.isNullOrBlank()) {
196 | when {
197 | args.isWebUrl() -> {
198 | scope.launch {
199 | val downloadItem = unrestrictLink(args)
200 | if (downloadItem != null) {
201 | val itemMessage: String =
202 | formatDownloadItem(downloadItem, allowTranscoding = true)
203 |
204 | bot.sendMessage(
205 | chatId = ChatId.fromId(message.chat.id),
206 | text = itemMessage,
207 | parseMode = ParseMode.MARKDOWN
208 | )
209 | }
210 | }
211 | }
212 | args.isMagnet() -> {
213 | scope.launch {
214 | val addedMagnet: UploadedTorrent? = addMagnet(args)
215 | if (addedMagnet != null) {
216 | val magnetMessage = localization.addedTorrent.replace("%id%", addedMagnet.id)
217 |
218 | bot.sendMessage(
219 | chatId = ChatId.fromId(message.chat.id),
220 | text = magnetMessage
221 | )
222 |
223 | fetchTorrentInfo(addedMagnet.id)
224 | }
225 | }
226 | }
227 | args.isTorrent() -> {
228 | val loaded = downloadTorrent(args)
229 | if (loaded)
230 | bot.sendMessage(
231 | chatId = ChatId.fromId(message.chat.id),
232 | text = localization.uploadingTorrent
233 | )
234 | }
235 | else -> bot.sendMessage(
236 | chatId = ChatId.fromId(message.chat.id),
237 | text = localization.wrongUnrestrictSyntax
238 | )
239 | }
240 | } else
241 | bot.sendMessage(
242 | chatId = ChatId.fromId(message.chat.id),
243 | text = localization.wrongUnrestrictSyntax
244 | )
245 | }
246 |
247 | message(transcodeCommandFilter and userFilter) {
248 | val args = getArgAsString(message.text)
249 | if (!args.isNullOrBlank()) {
250 |
251 | scope.launch {
252 | val streams: Stream? = streamLink(args)
253 | if (streams != null) {
254 | val streamsMessage = """
255 | ${localization.appleQuality}: ${streams.apple.link}
256 | ${localization.dashQuality}: ${streams.dash.link}
257 | ${localization.liveMP4Quality}: ${streams.liveMP4.link}
258 | ${localization.h264WebMQuality}: ${streams.h264WebM.link}
259 | """.trimIndent()
260 |
261 | bot.sendMessage(
262 | chatId = ChatId.fromId(message.chat.id),
263 | text = streamsMessage,
264 | parseMode = ParseMode.MARKDOWN
265 | )
266 | }
267 | }
268 | } else
269 | bot.sendMessage(
270 | chatId = ChatId.fromId(message.chat.id),
271 | text = localization.wrongStreamSyntax
272 | )
273 | }
274 |
275 | message(getCommandFilter and userFilter) {
276 | val args = getArgAsString(message.text)
277 | // todo: restrict link to real debrid urls?
278 | if (!args.isNullOrBlank() && args.isWebUrl()) {
279 | bot.sendMessage(
280 | chatId = ChatId.fromId(message.chat.id),
281 | text = localization.startingDownload
282 | )
283 | downloadScope.launch {
284 | withContext(Dispatchers.IO) {
285 | val process = ProcessBuilder(
286 | "wget",
287 | "-P", downloadsPath,
288 | wgetArguments,
289 | args
290 | ).redirectOutput(ProcessBuilder.Redirect.PIPE)
291 | .start()
292 | val reader = process.errorStream.bufferedReader(Charset.defaultCharset())
293 | reader.use {
294 | var line = it.readLine()
295 | while (line != null) {
296 | println(line)
297 | line = it.readLine()
298 | if (line != null)
299 | bot.sendMessage(
300 | chatId = ChatId.fromId(message.chat.id),
301 | text = line
302 | )
303 | }
304 | }
305 | process.waitFor()
306 | }
307 | }
308 | } else
309 | bot.sendMessage(
310 | chatId = ChatId.fromId(message.chat.id),
311 | text = localization.wrongDownloadSyntax
312 | )
313 | }
314 |
315 | message(torrentsCommandFilter and userFilter) {
316 | val args = getArgAsInt(message.text)
317 | scope.launch {
318 | val retrievedTorrents = args ?: 5
319 | val torrents: List =
320 | torrentsRepository.getTorrentsList(privateApiKey, 0, 1, retrievedTorrents, null)
321 | val stringBuilder = StringBuilder()
322 | torrents.forEach {
323 |
324 | val tempBuffer = StringBuffer()
325 |
326 | tempBuffer.append(
327 | """
328 | ${localization.name}: ${it.filename}
329 | ${localization.size}: ${it.bytes / 1024 / 1024} MB
330 | ${localization.status}: ${it.status}
331 | ${localization.progress}: ${it.progress}%
332 | """.trimIndent()
333 | )
334 | tempBuffer.appendLine()
335 | if (it.links.isNotEmpty()) {
336 | if (it.links.size == 1) {
337 | tempBuffer.append("${localization.getDownloadLink}\n/unrestrict ${it.links.first()}\n")
338 | } else {
339 | tempBuffer.append("${localization.getDownloadLink}\n")
340 | it.links.forEach { link ->
341 | tempBuffer.append("/unrestrict ")
342 | tempBuffer.append(link)
343 | tempBuffer.appendLine()
344 | }
345 | }
346 | }
347 | tempBuffer.appendLine()
348 |
349 | // if the size of the message is too big send a message and clear the main builder
350 | if (stringBuilder.length + tempBuffer.length > 4000) {
351 | bot.sendMessage(
352 | chatId = ChatId.fromId(message.chat.id),
353 | text = stringBuilder.toString(),
354 | disableWebPagePreview = true
355 | )
356 | stringBuilder.clear()
357 | }
358 | // add the current message to the main builder
359 | stringBuilder.append(tempBuffer)
360 | }
361 | bot.sendMessage(
362 | chatId = ChatId.fromId(message.chat.id),
363 | text = stringBuilder.toString(),
364 | disableWebPagePreview = true
365 | )
366 | }
367 | }
368 |
369 | message(downloadsCommandFilter and userFilter) {
370 | val args = getArgAsInt(message.text)
371 | scope.launch {
372 | val retrievedDownloads = args ?: 5
373 | val downloads: List =
374 | downloadRepository.getDownloads(privateApiKey, limit = retrievedDownloads)
375 | val stringBuilder = StringBuilder()
376 | downloads.forEach {
377 | val tempBuffer = StringBuilder()
378 | tempBuffer.append(formatDownloadItem(it))
379 | tempBuffer.appendLine()
380 | tempBuffer.appendLine()
381 |
382 | if (stringBuilder.length + tempBuffer.length > 4000) {
383 | bot.sendMessage(
384 | chatId = ChatId.fromId(message.chat.id),
385 | text = stringBuilder.toString(),
386 | parseMode = ParseMode.MARKDOWN
387 | )
388 | stringBuilder.clear()
389 | }
390 | stringBuilder.append(tempBuffer)
391 | }
392 | bot.sendMessage(
393 | chatId = ChatId.fromId(message.chat.id),
394 | text = stringBuilder.toString(),
395 | parseMode = ParseMode.MARKDOWN
396 | )
397 | }
398 | }
399 |
400 | // todo: add torrents and magnet support, add downloads and torrents list from the menu as items if the query is empty
401 | // decide what to do with authentication, use env variable
402 | if (enableQueriesArgument) {
403 | // N.B: you need to enable the inlining with BotFather using `/setinline` to use this
404 | inlineQuery {
405 | val queryText = inlineQuery.query
406 |
407 | if (queryText.isBlank() or queryText.isEmpty()) return@inlineQuery
408 |
409 | if (!queryText.isWebUrl()) return@inlineQuery
410 |
411 | scope.launch {
412 | val downloadItem = unrestrictLink(queryText)
413 | if (downloadItem != null) {
414 | val itemMessage: String = formatDownloadItem(downloadItem)
415 |
416 | val inlineResults = listOf(
417 | InlineQueryResult.Article(
418 | id = "1",
419 | title = localization.unrestrict,
420 | inputMessageContent = InputMessageContent.Text(
421 | itemMessage,
422 | parseMode = ParseMode.MARKDOWN
423 | ),
424 | description = localization.unrestrictDescription,
425 | url = null
426 | )
427 | )
428 |
429 | bot.answerInlineQuery(inlineQuery.id, inlineResults)
430 |
431 | } else {
432 | val inlineResults = listOf(
433 | InlineQueryResult.Article(
434 | id = "1",
435 | title = localization.error,
436 | inputMessageContent = InputMessageContent.Text(
437 | localization.unrestrictError,
438 | parseMode = ParseMode.MARKDOWN
439 | ),
440 | description = localization.unrestrictError,
441 | url = null
442 | )
443 | )
444 |
445 | bot.answerInlineQuery(inlineQuery.id, inlineResults)
446 | }
447 | }
448 | }
449 | }
450 | }
451 | }
452 |
453 | bot.startPolling()
454 |
455 | println(localization.botStarted)
456 | }
457 |
458 | private fun printCurrentParameters() {
459 | println(
460 | """
461 | ******************
462 | * BOT PARAMETERS *
463 | ******************
464 | """.trimIndent()
465 | )
466 | println("Wget arguments: $wgetArguments")
467 | println("Log level: $logLevelArgument")
468 | println("Queries enabled: $enableQueriesArgument")
469 | val hasUser = whitelistedUser > 1000
470 | println("Whitelisted user: $hasUser")
471 | println("Localization: $localeArgument")
472 | println("Temp file path: $tempPath")
473 | println("Downloaded files path: $downloadsPath \n")
474 | }
475 |
476 | private fun formatDownloadItem(item: DownloadItem, allowTranscoding: Boolean = false): String {
477 | // todo: add keyboard to launch transcoding instructions
478 | return "*${localization.name}:* ${item.filename}\n" +
479 | "*${localization.size}:* ${item.fileSize / 1024 / 1024} MB\n" +
480 | (
481 | if (allowTranscoding) {
482 | if (item.streamable == 1)
483 | "${localization.transcodingInstructions} /transcode ${item.id}\n"
484 | else
485 | "*${localization.streamingUnavailable}*\n"
486 | } else
487 | ""
488 | ) +
489 | "*${localization.link}:* ${item.download}"
490 | }
491 |
492 | private fun checkAndMakeDirectories(vararg paths: String) {
493 | // todo: add checks if path is file and if path is writeable
494 | paths.forEach {
495 | val directory = File(it)
496 | if (!directory.exists())
497 | directory.mkdir()
498 | }
499 | }
500 |
501 | private suspend fun getUser(): User? {
502 | return userRepository.getUserInfo(privateApiKey)
503 | }
504 |
505 | private suspend fun unrestrictLink(link: String): DownloadItem? {
506 | return unrestrictRepository.getUnrestrictedLink(privateApiKey, link)
507 | }
508 |
509 | private suspend fun streamLink(id: String): Stream? {
510 | return streamingRepository.getStreams(privateApiKey, id)
511 | }
512 |
513 | private suspend fun addMagnet(magnet: String): UploadedTorrent? {
514 | val availableHosts = torrentsRepository.getAvailableHosts(privateApiKey)
515 | return if (!availableHosts.isNullOrEmpty()) {
516 | torrentsRepository.addMagnet(privateApiKey, magnet, availableHosts.first().host)
517 | } else
518 | null
519 | }
520 |
521 | private fun fetchTorrentInfo(torrentID: String, delay: Long = 2000) {
522 | scope.launch {
523 | delay(delay)
524 | torrentsRepository.getTorrentInfo(privateApiKey, torrentID)?.let {
525 | // the download won't start if we don't select the files
526 | if (it.status == "waiting_files_selection")
527 | torrentsRepository.selectFiles(privateApiKey, torrentID)
528 | // has the magnet/torrent been loaded?
529 | if (initialStatusList.contains(it.status))
530 | fetchTorrentInfo(it.id)
531 | // other statuses means the download is either started, finished or failed and require no intervention
532 | }
533 | }
534 | }
535 |
536 | private fun downloadTorrent(link: String): Boolean {
537 | val downloadRequest: Request = Request.Builder().url(link).get().build()
538 |
539 | val response = okHttpClient.newCall(downloadRequest).execute()
540 | if (!response.isSuccessful) {
541 | return false
542 | }
543 | val source = response.body?.source()
544 | if (source != null) {
545 | val path = (if (tempPath.endsWith("/")) tempPath else tempPath.plus("/")) + link.hashCode() + ".torrent"
546 | println("Saving torrent file")
547 | val file = File(path)
548 | val bufferedSink = file.sink().buffer()
549 | bufferedSink.writeAll(source)
550 | bufferedSink.close()
551 |
552 | val fileInputStream = file.inputStream()
553 | val buffer: ByteArray = fileInputStream.readBytes()
554 | fileInputStream.close()
555 |
556 | uploadTorrent(buffer)
557 |
558 | return true
559 | } else
560 | return false
561 | }
562 |
563 | private fun uploadTorrent(buffer: ByteArray) {
564 | println("Uploading torrent to Real Debrid")
565 | scope.launch {
566 | val availableHosts = torrentsRepository.getAvailableHosts(privateApiKey)
567 | if (!availableHosts.isNullOrEmpty()) {
568 | val uploadedTorrent = torrentsRepository.addTorrent(privateApiKey, buffer, availableHosts.first().host)
569 | if (uploadedTorrent != null)
570 | fetchTorrentInfo(uploadedTorrent.id)
571 | }
572 | }
573 | }
574 |
575 | private fun getArgAsInt(args: String?): Int? {
576 | return args?.split("\\s+".toRegex())?.drop(1)?.firstOrNull()?.toIntOrNull()
577 | }
578 |
579 | private fun getArgsAsList(args: String?): List {
580 | return args?.split("\\s+".toRegex())?.drop(1) ?: emptyList()
581 | }
582 |
583 | private fun getArgAsString(args: String?): String? {
584 | return args?.split("\\s+".toRegex())?.drop(1)?.firstOrNull()
585 | }
586 | }
587 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot
2 |
3 | const val OPEN_SOURCE_CLIENT_ID = "X245A4XAIBGVM"
4 |
5 | const val OPEN_SOURCE_GRANT_TYPE = "http://oauth.net/grant_type/device/1.0"
6 |
7 | const val BASE_URL = "https://api.real-debrid.com/rest/1.0/"
8 |
9 | const val BASE_AUTH_URL = "https://api.real-debrid.com/oauth/v2/"
10 |
11 | const val MAGNET_PATTERN: String = "magnet:\\?xt=urn:btih:[a-zA-Z0-9]{32}"
12 | const val TORRENT_PATTERN: String = "https?://[^\\s]{7,}.torrent"
13 |
14 | const val FEEDBACK_URL = "https://github.com/LivingWithHippos/unchained-android"
15 | const val GPLV3_URL = "https://www.gnu.org/licenses/gpl-3.0.en.html"
16 |
17 | const val PRIVATE_TOKEN: String = "private_token"
18 |
19 | const val CREDENTIALS_PATH = "./credentials.json"
20 |
21 | val errorMap = mapOf(
22 | -1 to "Internal error",
23 | 1 to "Missing parameter",
24 | 2 to "Bad parameter value",
25 | 3 to "Unknown method",
26 | 4 to "Method not allowed",
27 | 5 to "Slow down",
28 | 6 to "Resource unreachable",
29 | 7 to "Resource not found",
30 | 8 to "Bad token",
31 | 9 to "Permission denied",
32 | 10 to "Two-Factor authentication needed",
33 | 11 to "Two-Factor authentication pending",
34 | 12 to "Invalid login",
35 | 13 to "Invalid password",
36 | 14 to "Account locked",
37 | 15 to "Account not activated",
38 | 16 to "Unsupported hoster",
39 | 17 to "Hoster in maintenance",
40 | 18 to "Hoster limit reached",
41 | 19 to "Hoster temporarily unavailable",
42 | 20 to "Hoster not available for free users",
43 | 21 to "Too many active downloads",
44 | 22 to "IP Address not allowed",
45 | 23 to "Traffic exhausted",
46 | 24 to "File unavailable",
47 | 25 to "Service unavailable",
48 | 26 to "Upload too big",
49 | 27 to "Upload error",
50 | 28 to "File not allowed",
51 | 29 to "Torrent too big",
52 | 30 to "Torrent file invalid",
53 | 31 to "Action already done",
54 | 32 to "Image resolution error",
55 | 33 to "Torrent already active"
56 | )
57 |
58 | // Torrent status list
59 |
60 | val endedStatusList = listOf("magnet_error", "downloaded", "error", "virus", "dead")
61 |
62 | // possible status are magnet_error, magnet_conversion, waiting_files_selection,
63 | // queued, downloading, downloaded, error, virus, compressing, uploading, dead
64 | val loadingStatusList = listOf(
65 | "downloading",
66 | "magnet_conversion",
67 | "waiting_files_selection",
68 | "queued",
69 | "compressing",
70 | "uploading"
71 | )
72 |
73 | val initialStatusList = listOf(
74 | "magnet_conversion",
75 | "queued",
76 | "waiting_files_selection"
77 | )
78 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/Main.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot
2 |
3 | import com.github.livingwithhippos.unchained_bot.di.myModules
4 | import org.koin.core.context.startKoin
5 | import org.koin.core.logger.Level
6 | import org.koin.environmentProperties
7 |
8 | fun main(args: Array) {
9 |
10 | val koinInstance = startKoin {
11 | // enable Printlogger with default Level.INFO
12 | // can have Level & implementation
13 | // equivalent to logger(Level.DEBUG, PrintLogger())
14 | printLogger(Level.ERROR)
15 |
16 | // declare properties from given map
17 | // properties()
18 |
19 | // load properties from koin.properties file or given file name
20 | // fileProperties()
21 |
22 | // load properties from environment
23 | // use this to load docker env variables, use getProperty("bot_token")?:"default_value"
24 | environmentProperties()
25 |
26 | // list all used modules
27 | // as list or vararg
28 | modules(myModules)
29 | }
30 |
31 | val botApp = BotApplication()
32 | botApp.startBot()
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/APIError.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class APIError(
8 | @Json(name = "error")
9 | val error: String,
10 | @Json(name = "error_details")
11 | val errorDetails: String?,
12 | @Json(name = "error_code")
13 | val errorCode: Int?
14 | ) : UnchainedNetworkException
15 |
16 | // todo: this has been resolved by adding an interceptor, change class name at least
17 | data class EmptyBodyError(
18 | val returnCode: Int
19 | ) : UnchainedNetworkException
20 |
21 | data class NetworkError(
22 | val error: Int,
23 | val message: String
24 | ) : UnchainedNetworkException
25 |
26 | data class ApiConversionError(
27 | val error: Int
28 | ) : UnchainedNetworkException
29 |
30 | interface UnchainedNetworkException
31 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/Authentication.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | /**
7 | * This class correspond to the JSON response from the authentication endpoint that starts the authentication process.
8 | */
9 | @JsonClass(generateAdapter = true)
10 | data class Authentication(
11 | @Json(name = "device_code")
12 | val deviceCode: String,
13 | @Json(name = "user_code")
14 | val userCode: String,
15 | @Json(name = "interval")
16 | val interval: Int,
17 | @Json(name = "expires_in")
18 | val expiresIn: Int,
19 | @Json(name = "verification_url")
20 | val verificationUrl: String,
21 | @Json(name = "direct_verification_url")
22 | val directVerificationUrl: String
23 | )
24 |
25 | /**
26 | * This class correspond to the JSON response from the secret endpoint, the second step in the authentication process.
27 | */
28 | @JsonClass(generateAdapter = true)
29 | data class Secrets(
30 | @Json(name = "client_id")
31 | val clientId: String,
32 | @Json(name = "client_secret")
33 | val clientSecret: String
34 | )
35 |
36 | /**
37 | * This class correspond to the JSON response from the token endpoint, the third and last step in the authentication process.
38 | * It can also be used to refresh an expired token
39 | */
40 | @JsonClass(generateAdapter = true)
41 | data class Token(
42 | @Json(name = "access_token")
43 | val accessToken: String,
44 | @Json(name = "expires_in")
45 | val expiresIn: Int,
46 | @Json(name = "token_type")
47 | val tokenType: String,
48 | @Json(name = "refresh_token")
49 | val refreshToken: String
50 | )
51 |
52 | /**
53 | * Enum class that represents the possible authentication states
54 | */
55 | enum class AuthenticationState {
56 | AUTHENTICATED, UNAUTHENTICATED, BAD_TOKEN, ACCOUNT_LOCKED, AUTHENTICATED_NO_PREMIUM
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/Credentials.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | /**
7 | * The credentials db entity.
8 | * It can store either a token obtained with the oauth process or a private api token which will populate all fields but accessToken with PRIVATE_TOKEN
9 | */
10 |
11 | @JsonClass(generateAdapter = true)
12 | data class Credentials(
13 | @Json(name = "device_code")
14 | val deviceCode: String,
15 | @Json(name = "client_id")
16 | val clientId: String?,
17 | @Json(name = "client_secret")
18 | val clientSecret: String?,
19 | @Json(name = "access_token")
20 | val accessToken: String?,
21 | @Json(name = "refresh_token")
22 | val refreshToken: String?
23 | )
24 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/DownloadItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | /*
7 | [
8 | {
9 | "id": "string",
10 | "filename": "string",
11 | "mimeType": "string", // Mime Type of the file, guessed by the file extension
12 | "filesize": int, // bytes, 0 if unknown
13 | "link": "string", // Original link
14 | "host": "string", // Host main domain
15 | "chunks": int, // Max Chunks allowed
16 | "download": "string", // Generated link
17 | "generated": "string" // jsonDate
18 | },
19 | {
20 | "id": "string",
21 | "filename": "string",
22 | "mimeType": "string",
23 | "filesize": int,
24 | "link": "string",
25 | "host": "string",
26 | "chunks": int,
27 | "download": "string",
28 | "generated": "string",
29 | "type": "string" // Type of the file (in general, its quality)
30 | }
31 | ]
32 | */
33 |
34 | @JsonClass(generateAdapter = true)
35 | data class DownloadItem(
36 | @Json(name = "id")
37 | val id: String,
38 | @Json(name = "filename")
39 | val filename: String,
40 | @Json(name = "mimeType")
41 | val mimeType: String?,
42 | @Json(name = "filesize")
43 | val fileSize: Long,
44 | @Json(name = "link")
45 | val link: String,
46 | @Json(name = "host")
47 | val host: String,
48 | @Json(name = "host_icon")
49 | val hostIcon: String?,
50 | @Json(name = "chunks")
51 | val chunks: Int,
52 | @Json(name = "crc")
53 | val crc: Int?,
54 | @Json(name = "download")
55 | val download: String,
56 | @Json(name = "streamable")
57 | val streamable: Int?,
58 | @Json(name = "generated")
59 | val generated: String?,
60 | @Json(name = "type")
61 | val type: String?,
62 | @Json(name = "alternative")
63 | val alternative: List?
64 | )
65 |
66 | @JsonClass(generateAdapter = true)
67 | data class Alternative(
68 | @Json(name = "id")
69 | val id: String,
70 | @Json(name = "filename")
71 | val filename: String,
72 | @Json(name = "download")
73 | val download: String,
74 | @Json(name = "mimeType")
75 | val mimeType: String?,
76 | @Json(name = "quality")
77 | val quality: String?
78 | )
79 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/EmptyBodyInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import okhttp3.Interceptor
4 | import okhttp3.MediaType.Companion.toMediaType
5 | import okhttp3.Response
6 | import okhttp3.ResponseBody.Companion.toResponseBody
7 |
8 | object EmptyBodyInterceptor : Interceptor {
9 |
10 | override fun intercept(chain: Interceptor.Chain): Response {
11 | val response = chain.proceed(chain.request())
12 | if (!response.isSuccessful || response.code.let { it != 204 && it != 205 }) {
13 | return response
14 | }
15 |
16 | if ((response.body?.contentLength() ?: -1) >= 0) {
17 | return response.newBuilder().code(200).build()
18 | }
19 |
20 | // optionally we could return a new JSON item with the original code in it
21 | // val emptyBody = "{original_code: ${response.code} }".toResponseBody("application/json".toMediaType())
22 |
23 | val emptyBody = "".toResponseBody("text/plain".toMediaType())
24 |
25 | return response
26 | .newBuilder()
27 | .code(200)
28 | .body(emptyBody)
29 | .build()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/Host.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | // fixme: the key is variable, the same as HostStatus.name, find out how to model that @Json(name = ?)
7 | @JsonClass(generateAdapter = true)
8 | data class Host(
9 | @Json(name = "id")
10 | val hostStatus: Map
11 | )
12 |
13 | @JsonClass(generateAdapter = true)
14 | data class HostStatus(
15 | @Json(name = "id")
16 | val id: String,
17 | @Json(name = "name")
18 | val name: String,
19 | @Json(name = "image")
20 | val image: String,
21 | @Json(name = "image_big")
22 | val imageBig: String,
23 | @Json(name = "supported")
24 | val supported: String,
25 | @Json(name = "status")
26 | val status: String,
27 | @Json(name = "check_time")
28 | val checkTime: String,
29 | @Json(name = "competitors_status")
30 | val competitorsStatus: Map
31 | )
32 |
33 | @JsonClass(generateAdapter = true)
34 | data class Competitor(
35 | @Json(name = "status")
36 | val status: String,
37 | @Json(name = "check_time")
38 | val checkTime: String
39 | )
40 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/NetworkResponse.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | /*
4 | * see https://kotlinlang.org/docs/reference/generics.html#declaration-site-variance for an explanation of the out keyword.
5 | * T and U will be used as return types
6 | */
7 |
8 | // todo: add loading class
9 | /**
10 | * Sealed classes representing all the possible network responses
11 | */
12 | sealed class NetworkResponse {
13 | data class Success(val data: T) : NetworkResponse()
14 | data class SuccessEmptyBody(val code: Int) : NetworkResponse()
15 | data class Error(val exception: Exception) : NetworkResponse()
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/Stream.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class Stream(
8 | @Json(name = "apple")
9 | val apple: Quality,
10 | @Json(name = "dash")
11 | val dash: Quality,
12 | @Json(name = "liveMP4")
13 | val liveMP4: Quality,
14 | @Json(name = "h264WebM")
15 | val h264WebM: Quality
16 | )
17 |
18 | @JsonClass(generateAdapter = true)
19 | data class Quality(
20 | @Json(name = "full")
21 | val link: String
22 | )
23 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/TorrentItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | /*
7 | [
8 | {
9 | "id": "string",
10 | "filename": "string",
11 | "original_filename": "string", // Original name of the torrent
12 | "hash": "string", // SHA1 Hash of the torrent
13 | "bytes": int, // Size of selected files only
14 | "original_bytes": int, // Total size of the torrent
15 | "host": "string", // Host main domain
16 | "split": int, // Split size of links
17 | "progress": int, // Possible values: 0 to 100
18 | "status": "downloaded", // Current status of the torrent: magnet_error, magnet_conversion, waiting_files_selection, queued, downloading, downloaded, error, virus, compressing, uploading, dead
19 | "added": "string", // jsonDate
20 | "files": [
21 | {
22 | "id": int,
23 | "path": "string", // Path to the file inside the torrent, starting with "/"
24 | "bytes": int,
25 | "selected": int // 0 or 1
26 | },
27 | {
28 | "id": int,
29 | "path": "string", // Path to the file inside the torrent, starting with "/"
30 | "bytes": int,
31 | "selected": int // 0 or 1
32 | }
33 | ],
34 | "links": [
35 | "string" // Host URL
36 | ],
37 | "ended": "string", // !! Only present when finished, jsonDate
38 | "speed": int, // !! Only present in "downloading", "compressing", "uploading" status
39 | "seeders": int // !! Only present in "downloading", "magnet_conversion" status
40 | }
41 | ]
42 | */
43 |
44 | @JsonClass(generateAdapter = true)
45 | data class TorrentItem(
46 | @Json(name = "id")
47 | val id: String,
48 | @Json(name = "filename")
49 | val filename: String,
50 | @Json(name = "original_filename")
51 | val originalFilename: String?,
52 | @Json(name = "hash")
53 | val hash: String,
54 | @Json(name = "bytes")
55 | val bytes: Long,
56 | @Json(name = "original_bytes")
57 | val originalBytes: Long?,
58 | @Json(name = "host")
59 | val host: String,
60 | @Json(name = "split")
61 | val split: Int,
62 | @Json(name = "progress")
63 | val progress: Int,
64 | @Json(name = "status")
65 | val status: String,
66 | @Json(name = "added")
67 | val added: String,
68 | @Json(name = "files")
69 | val files: List?,
70 | @Json(name = "links")
71 | val links: List,
72 | @Json(name = "ended")
73 | val ended: String?,
74 | @Json(name = "speed")
75 | val speed: Int?,
76 | @Json(name = "seeders")
77 | val seeders: Int?
78 | )
79 |
80 | @JsonClass(generateAdapter = true)
81 | data class InnerTorrentFile(
82 | @Json(name = "id")
83 | val id: Int,
84 | @Json(name = "path")
85 | val path: String,
86 | @Json(name = "bytes")
87 | val bytes: Long,
88 | @Json(name = "selected")
89 | val selected: Int
90 | )
91 |
92 | @JsonClass(generateAdapter = true)
93 | data class UploadedTorrent(
94 | @Json(name = "id")
95 | val id: String,
96 | @Json(name = "uri")
97 | val uri: String
98 | )
99 |
100 | @JsonClass(generateAdapter = true)
101 | data class AvailableHost(
102 | @Json(name = "host")
103 | val host: String,
104 | @Json(name = "max_file_size")
105 | val maxFileSize: Int
106 | )
107 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/model/User.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.model
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class User(
8 | @Json(name = "id")
9 | val id: Int,
10 | @Json(name = "username")
11 | val username: String,
12 | @Json(name = "email")
13 | val email: String,
14 | @Json(name = "points")
15 | val points: Int,
16 | @Json(name = "locale")
17 | val locale: String,
18 | @Json(name = "avatar")
19 | val avatar: String,
20 | @Json(name = "type")
21 | val type: String,
22 | @Json(name = "premium")
23 | val premium: Int,
24 | @Json(name = "expiration")
25 | val expiration: String
26 | )
27 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/AuthApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Authentication
4 | import com.github.livingwithhippos.unchained_bot.data.model.Secrets
5 | import com.github.livingwithhippos.unchained_bot.data.model.Token
6 | import retrofit2.Response
7 |
8 | interface AuthApiHelper {
9 |
10 | suspend fun getAuthentication(): Response
11 |
12 | suspend fun getSecrets(
13 | deviceCode: String
14 | ): Response
15 |
16 | suspend fun getToken(
17 | clientId: String,
18 | clientSecret: String,
19 | code: String
20 | ): Response
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/AuthApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Authentication
4 | import com.github.livingwithhippos.unchained_bot.data.model.Secrets
5 | import com.github.livingwithhippos.unchained_bot.data.model.Token
6 | import retrofit2.Response
7 |
8 | class AuthApiHelperImpl(private val authenticationApi: AuthenticationApi) :
9 | AuthApiHelper {
10 |
11 | override suspend fun getAuthentication(): Response =
12 | authenticationApi.getAuthentication()
13 |
14 | override suspend fun getSecrets(deviceCode: String): Response =
15 | authenticationApi.getSecrets(deviceCode = deviceCode)
16 |
17 | override suspend fun getToken(
18 | clientId: String,
19 | clientSecret: String,
20 | code: String
21 | ): Response = authenticationApi.getToken(clientId, clientSecret, code)
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/AuthenticationApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.OPEN_SOURCE_CLIENT_ID
4 | import com.github.livingwithhippos.unchained_bot.OPEN_SOURCE_GRANT_TYPE
5 | import com.github.livingwithhippos.unchained_bot.data.model.Authentication
6 | import com.github.livingwithhippos.unchained_bot.data.model.Secrets
7 | import com.github.livingwithhippos.unchained_bot.data.model.Token
8 | import retrofit2.Response
9 | import retrofit2.http.Field
10 | import retrofit2.http.FormUrlEncoded
11 | import retrofit2.http.GET
12 | import retrofit2.http.POST
13 | import retrofit2.http.Query
14 |
15 | /**
16 | * This interface is used by Retrofit to manage all the REST calls to the endpoints needed to authenticate the user
17 | */
18 | interface AuthenticationApi {
19 |
20 | @GET("device/code")
21 | suspend fun getAuthentication(
22 | @Query("client_id") id: String = OPEN_SOURCE_CLIENT_ID,
23 | @Query("new_credentials") newCredentials: String = "yes"
24 | ): Response
25 |
26 | @GET("device/credentials")
27 | suspend fun getSecrets(
28 | @Query("client_id") id: String = OPEN_SOURCE_CLIENT_ID,
29 | @Query("code") deviceCode: String
30 | ): Response
31 |
32 | /**
33 | * This is also used to refresh the token.
34 | */
35 | @FormUrlEncoded
36 | @POST("token")
37 | suspend fun getToken(
38 | @Field("client_id") clientId: String,
39 | @Field("client_secret") clientSecret: String,
40 | @Field("code") code: String,
41 | @Field("grant_type") grantType: String = OPEN_SOURCE_GRANT_TYPE
42 | ): Response
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/DownloadApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import retrofit2.Response
5 |
6 | interface DownloadApiHelper {
7 | suspend fun getDownloads(
8 | token: String,
9 | offset: Int?,
10 | page: Int,
11 | limit: Int
12 | ): Response>
13 |
14 | suspend fun deleteDownload(
15 | token: String,
16 | id: String,
17 | ): Response
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/DownloadApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import retrofit2.Response
5 |
6 | class DownloadApiHelperImpl(private val downloadsApi: DownloadsApi) :
7 | DownloadApiHelper {
8 |
9 | override suspend fun getDownloads(
10 | token: String,
11 | offset: Int?,
12 | page: Int,
13 | limit: Int
14 | ): Response> =
15 | downloadsApi.getDownloads(token, offset, page, limit)
16 |
17 | override suspend fun deleteDownload(token: String, id: String) =
18 | downloadsApi.deleteDownload(token, id)
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/DownloadsApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import retrofit2.Response
5 | import retrofit2.http.DELETE
6 | import retrofit2.http.GET
7 | import retrofit2.http.Header
8 | import retrofit2.http.Path
9 | import retrofit2.http.Query
10 |
11 | interface DownloadsApi {
12 |
13 | /**
14 | * Get user downloads list
15 | * You can not use both offset and page at the same time, page is prioritized in case it happens.
16 | * @param token the authorization token, formed as "Bearer api_token"
17 | * @param offset Starting offset (must be within 0 and X-Total-Count HTTP header)
18 | * @param page Page number
19 | * @param limit Entries returned per page / request (must be within 0 and 100, default: 50)
20 | * @return a Response> a list of download items
21 | */
22 | @GET("downloads")
23 | suspend fun getDownloads(
24 | @Header("Authorization") token: String,
25 | @Query("offset") offset: Int?,
26 | @Query("page") page: Int,
27 | @Query("limit") limit: Int = 50
28 | ): Response>
29 |
30 | /**
31 | * Delete a download.
32 | * @param token the authorization token, formed as "Bearer api_token"
33 | * @param id the download ID, returned by getDownloads
34 | */
35 | @DELETE("downloads/delete/{id}")
36 | suspend fun deleteDownload(
37 | @Header("Authorization") token: String,
38 | @Path("id") id: String,
39 | ): Response
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/HostsApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Host
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 | import retrofit2.http.Header
7 |
8 | interface HostsApi {
9 |
10 | @GET("hosts/status/")
11 | suspend fun getStreams(
12 | @Header("Authorization") token: String
13 | ): Response
14 |
15 | @GET("hosts/regex/")
16 | suspend fun getHostsRegex(): Response>
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/HostsApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Host
4 | import retrofit2.Response
5 |
6 | interface HostsApiHelper {
7 | suspend fun getHostsStatus(token: String): Response
8 | suspend fun getHostsRegex(): Response>
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/HostsApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Host
4 | import retrofit2.Response
5 |
6 | class HostsApiHelperImpl(private val hostsApi: HostsApi) :
7 | HostsApiHelper {
8 |
9 | override suspend fun getHostsStatus(token: String): Response = hostsApi.getStreams(token)
10 |
11 | override suspend fun getHostsRegex(): Response> = hostsApi.getHostsRegex()
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/StreamingApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Stream
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 | import retrofit2.http.Header
7 | import retrofit2.http.Path
8 |
9 | /**
10 | * This interface is used by Retrofit to manage all the REST calls to the endpoints needed to retrieve streaming links from a download
11 | * The APIs in this interface will not work with an open source token.
12 | */
13 | interface StreamingApi {
14 |
15 | @GET("streaming/transcode/{id}")
16 | suspend fun getStreams(
17 | @Header("Authorization") token: String,
18 | @Path("id") id: String
19 | ): Response
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/StreamingApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Stream
4 | import retrofit2.Response
5 |
6 | interface StreamingApiHelper {
7 | suspend fun getStreams(token: String, id: String): Response
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/StreamingApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Stream
4 | import retrofit2.Response
5 |
6 | class StreamingApiHelperImpl(private val streamingApi: StreamingApi) :
7 | StreamingApiHelper {
8 | override suspend fun getStreams(token: String, id: String): Response =
9 | streamingApi.getStreams(token, id)
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/TorrentApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.AvailableHost
4 | import com.github.livingwithhippos.unchained_bot.data.model.TorrentItem
5 | import com.github.livingwithhippos.unchained_bot.data.model.UploadedTorrent
6 | import okhttp3.RequestBody
7 | import retrofit2.Response
8 |
9 | interface TorrentApiHelper {
10 |
11 | suspend fun getAvailableHosts(token: String): Response>
12 |
13 | suspend fun getTorrentInfo(
14 | token: String,
15 | id: String
16 | ): Response
17 |
18 | suspend fun addTorrent(
19 | token: String,
20 | binaryTorrent: RequestBody,
21 | host: String
22 | ): Response
23 |
24 | suspend fun addMagnet(
25 | token: String,
26 | magnet: String,
27 | host: String
28 | ): Response
29 |
30 | suspend fun getTorrentsList(
31 | token: String,
32 | offset: Int?,
33 | page: Int?,
34 | limit: Int?,
35 | filter: String?
36 | ): Response>
37 |
38 | suspend fun selectFiles(
39 | token: String,
40 | id: String,
41 | files: String
42 | ): Response
43 |
44 | suspend fun deleteTorrent(
45 | token: String,
46 | id: String
47 | ): Response
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/TorrentApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.AvailableHost
4 | import com.github.livingwithhippos.unchained_bot.data.model.TorrentItem
5 | import com.github.livingwithhippos.unchained_bot.data.model.UploadedTorrent
6 | import okhttp3.RequestBody
7 | import retrofit2.Response
8 |
9 | class TorrentApiHelperImpl(private val torrentsApi: TorrentsApi) :
10 | TorrentApiHelper {
11 | override suspend fun getAvailableHosts(token: String): Response> =
12 | torrentsApi.getAvailableHosts(token)
13 |
14 | override suspend fun getTorrentInfo(token: String, id: String): Response =
15 | torrentsApi.getTorrentInfo(token, id)
16 |
17 | override suspend fun addTorrent(
18 | token: String,
19 | binaryTorrent: RequestBody,
20 | host: String
21 | ): Response = torrentsApi.addTorrent(token, binaryTorrent, host)
22 |
23 | override suspend fun addMagnet(
24 | token: String,
25 | magnet: String,
26 | host: String
27 | ): Response = torrentsApi.addMagnet(token, magnet, host)
28 |
29 | override suspend fun getTorrentsList(
30 | token: String,
31 | offset: Int?,
32 | page: Int?,
33 | limit: Int?,
34 | filter: String?
35 | ): Response> = torrentsApi.getTorrentsList(token, offset, page, limit, filter)
36 |
37 | override suspend fun selectFiles(token: String, id: String, files: String) =
38 | torrentsApi.selectFiles(token, id, files)
39 |
40 | override suspend fun deleteTorrent(token: String, id: String) =
41 | torrentsApi.deleteTorrent(token, id)
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/TorrentsApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.AvailableHost
4 | import com.github.livingwithhippos.unchained_bot.data.model.TorrentItem
5 | import com.github.livingwithhippos.unchained_bot.data.model.UploadedTorrent
6 | import okhttp3.RequestBody
7 | import retrofit2.Response
8 | import retrofit2.http.Body
9 | import retrofit2.http.DELETE
10 | import retrofit2.http.Field
11 | import retrofit2.http.FormUrlEncoded
12 | import retrofit2.http.GET
13 | import retrofit2.http.Header
14 | import retrofit2.http.POST
15 | import retrofit2.http.PUT
16 | import retrofit2.http.Path
17 | import retrofit2.http.Query
18 |
19 | /**
20 | * This interface is used by Retrofit to manage all the REST calls to the torrents endpoints
21 | */
22 | interface TorrentsApi {
23 |
24 | @GET("torrents/availableHosts")
25 | suspend fun getAvailableHosts(
26 | @Header("Authorization") token: String
27 | ): Response>
28 |
29 | @GET("torrents/info/{id}")
30 | suspend fun getTorrentInfo(
31 | @Header("Authorization") token: String,
32 | @Path("id") id: String
33 | ): Response
34 |
35 | @PUT("torrents/addTorrent")
36 | suspend fun addTorrent(
37 | @Header("Authorization") token: String,
38 | @Body binaryTorrent: RequestBody,
39 | @Query("host") host: String
40 | ): Response
41 |
42 | @FormUrlEncoded
43 | @POST("torrents/addMagnet")
44 | suspend fun addMagnet(
45 | @Header("Authorization") token: String,
46 | @Field("magnet") magnet: String,
47 | @Field("host") host: String
48 | ): Response
49 |
50 | /**
51 | * Get a list of the user's torrents.
52 | * @param token the authorization token, formed as "Bearer api_token"
53 | * @param offset Starting offset (must be within 0 and X-Total-Count HTTP header)
54 | * @param page Page number
55 | * @param limit Entries returned per page / request (must be within 0 and 100, default: 50)
56 | * @param filter "active", list active torrents first
57 | * @return a Response> a list of torrent items
58 | */
59 | @GET("torrents")
60 | suspend fun getTorrentsList(
61 | @Header("Authorization") token: String,
62 | @Query("offset") offset: Int? = 0,
63 | @Query("page") page: Int? = 1,
64 | @Query("limit") limit: Int? = 10,
65 | @Query("filter") filter: String?
66 | ): Response>
67 |
68 | /**
69 | * Select files of a torrent. Required to start a torrent.
70 | * @param token the authorization token, formed as "Bearer api_token"
71 | * @param id the torrent ID, returned by addTorrent or getTorrentsList
72 | * @param files Selected files IDs (comma separated) or "all"
73 | */
74 | @FormUrlEncoded
75 | @POST("torrents/selectFiles/{id}")
76 | suspend fun selectFiles(
77 | @Header("Authorization") token: String,
78 | @Path("id") id: String,
79 | @Field("files") files: String = "all"
80 | ): Response
81 |
82 | /**
83 | * Delete a torrent.
84 | * @param token the authorization token, formed as "Bearer api_token"
85 | * @param id the torrent ID, returned by addTorrent or getTorrentsList
86 | */
87 | @DELETE("torrents/delete/{id}")
88 | suspend fun deleteTorrent(
89 | @Header("Authorization") token: String,
90 | @Path("id") id: String,
91 | ): Response
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/UnrestrictApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import retrofit2.Response
5 | import retrofit2.http.Field
6 | import retrofit2.http.FormUrlEncoded
7 | import retrofit2.http.Header
8 | import retrofit2.http.POST
9 |
10 | /*
11 | {
12 | "id": "string",
13 | "filename": "string",
14 | "mimeType": "string", // Mime Type of the file, guessed by the file extension
15 | "filesize": int, // Filesize in bytes, 0 if unknown
16 | "link": "string", // Original link
17 | "host": "string", // Host main domain
18 | "chunks": int, // Max Chunks allowed
19 | "crc": int, // Disable / enable CRC check
20 | "download": "string", // Generated link
21 | "streamable": int // Is the file streamable on website
22 | }
23 | */
24 |
25 | interface UnrestrictApi {
26 |
27 | /**
28 | * Unrestrict a hoster link and get a new unrestricted link
29 | *
30 | */
31 | @FormUrlEncoded
32 | @POST("unrestrict/link")
33 | suspend fun getUnrestrictedLink(
34 | @Header("Authorization") token: String,
35 | // The original hoster link
36 | @Field("link") link: String,
37 | // Password to unlock the file access hoster side
38 | @Field("password") password: String? = null,
39 | // 0 or 1, use Remote traffic, dedicated servers and account sharing protections lifted
40 | @Field("remote") remote: Int? = null
41 | ): Response
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/UnrestrictApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import retrofit2.Response
5 |
6 | interface UnrestrictApiHelper {
7 |
8 | suspend fun getUnrestrictedLink(
9 | token: String,
10 | link: String,
11 | password: String? = null,
12 | remote: Int? = null
13 | ): Response
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/UnrestrictApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import retrofit2.Response
5 |
6 | class UnrestrictApiHelperImpl(private val unrestrictApi: UnrestrictApi) :
7 | UnrestrictApiHelper {
8 | override suspend fun getUnrestrictedLink(
9 | token: String,
10 | link: String,
11 | password: String?,
12 | remote: Int?
13 | ): Response =
14 | unrestrictApi.getUnrestrictedLink(token, link, password, remote)
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/UserApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.User
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 | import retrofit2.http.Header
7 |
8 | interface UserApi {
9 | @GET("user")
10 | suspend fun getUser(@Header("Authorization") token: String): Response
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/UserApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.User
4 | import retrofit2.Response
5 |
6 | interface UserApiHelper {
7 |
8 | suspend fun getUser(token: String): Response
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/UserApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.User
4 | import retrofit2.Response
5 |
6 | class UserApiHelperImpl(private val userApi: UserApi) :
7 | UserApiHelper {
8 | override suspend fun getUser(token: String): Response = userApi.getUser(token)
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/VariousApi.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import retrofit2.Response
4 | import retrofit2.http.GET
5 | import retrofit2.http.Header
6 |
7 | /**
8 | * This interface is used by Retrofit to manage various api calls not fitting elsewhere
9 | */
10 | interface VariousApi {
11 | /**
12 | * Disable the current access token
13 | */
14 | @GET("disable_access_token")
15 | suspend fun disableToken(
16 | @Header("Authorization") token: String
17 | ): Response
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/VariousApiHelper.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import retrofit2.Response
4 |
5 | interface VariousApiHelper {
6 | suspend fun disableToken(token: String): Response
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/remote/VariousApiHelperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.remote
2 |
3 | import retrofit2.Response
4 |
5 | class VariousApiHelperImpl(private val variousApi: VariousApi) :
6 | VariousApiHelper {
7 | override suspend fun disableToken(token: String): Response =
8 | variousApi.disableToken(token)
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/AuthenticationRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Authentication
4 | import com.github.livingwithhippos.unchained_bot.data.model.Credentials
5 | import com.github.livingwithhippos.unchained_bot.data.model.Secrets
6 | import com.github.livingwithhippos.unchained_bot.data.model.Token
7 | import com.github.livingwithhippos.unchained_bot.data.remote.AuthApiHelper
8 |
9 | class AuthenticationRepository(private val apiHelper: AuthApiHelper) :
10 | BaseRepository() {
11 |
12 | suspend fun getVerificationCode(): Authentication? {
13 |
14 | val authResponse = safeApiCall(
15 | call = { apiHelper.getAuthentication() },
16 | errorMessage = "Error Fetching Authentication Info"
17 | )
18 |
19 | return authResponse
20 | }
21 |
22 | suspend fun getSecrets(code: String): Secrets? {
23 |
24 | val secretResponse = safeApiCall(
25 | call = { apiHelper.getSecrets(deviceCode = code) },
26 | errorMessage = "Error Fetching Secrets"
27 | )
28 |
29 | return secretResponse
30 | }
31 |
32 | suspend fun getToken(clientId: String, clientSecret: String, code: String): Token? {
33 |
34 | val tokenResponse = safeApiCall(
35 | call = {
36 | apiHelper.getToken(
37 | clientId = clientId,
38 | clientSecret = clientSecret,
39 | code = code
40 | )
41 | },
42 | errorMessage = "Error Fetching Token"
43 | )
44 |
45 | return tokenResponse
46 | }
47 |
48 | /**
49 | * Get a new open source Token that usually lasts one hour.
50 | * You can not use both offset and page at the same time, page is prioritized in case it happens.
51 | * @param clientId the client id obtained from the /device/credentials endpoint
52 | * @param clientSecret the code obtained from the /token endpoint
53 | * @param refreshToken the device code obtained from the /device/code endpoint
54 | * @return the new Token
55 | */
56 | suspend fun refreshToken(clientId: String, clientSecret: String, refreshToken: String): Token? =
57 | getToken(clientId, clientSecret, refreshToken)
58 |
59 | suspend fun refreshToken(credentials: Credentials): Token? =
60 | refreshToken(credentials.clientId!!, credentials.clientSecret!!, credentials.refreshToken!!)
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/BaseRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.APIError
4 | import com.github.livingwithhippos.unchained_bot.data.model.NetworkResponse
5 | import com.squareup.moshi.JsonAdapter
6 | import com.squareup.moshi.Moshi
7 | import retrofit2.Response
8 | import java.io.IOException
9 |
10 | /**
11 | * Base repository class to be extended by other repositories.
12 | * Manages the calls between retrofit and the actual repositories.
13 | */
14 | open class BaseRepository {
15 |
16 | // todo: inject this
17 | private val jsonAdapter: JsonAdapter = Moshi.Builder()
18 | .build()
19 | .adapter(APIError::class.java)
20 |
21 | suspend fun safeApiCall(call: suspend () -> Response, errorMessage: String): T? {
22 |
23 | val result: NetworkResponse = safeApiResult(call, errorMessage)
24 | var data: T? = null
25 |
26 | when (result) {
27 | is NetworkResponse.Success ->
28 | data = result.data
29 | is NetworkResponse.SuccessEmptyBody -> ""
30 | is NetworkResponse.Error -> ""
31 | }
32 |
33 | return data
34 | }
35 |
36 | private suspend fun safeApiResult(
37 | call: suspend () -> Response,
38 | errorMessage: String
39 | ): NetworkResponse {
40 | val response = call.invoke()
41 | if (response.isSuccessful) {
42 | val body = response.body()
43 | if (body != null)
44 | return NetworkResponse.Success(body)
45 | else
46 | return NetworkResponse.SuccessEmptyBody(response.code())
47 | }
48 |
49 | return NetworkResponse.Error(IOException("Error Occurred while getting api result, error : $errorMessage"))
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/CredentialsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.CREDENTIALS_PATH
4 | import com.github.livingwithhippos.unchained_bot.PRIVATE_TOKEN
5 | import com.github.livingwithhippos.unchained_bot.data.model.Credentials
6 | import com.squareup.moshi.JsonAdapter
7 | import com.squareup.moshi.Moshi
8 | import java.io.BufferedReader
9 | import java.io.File
10 |
11 | // Declares the DAO as a private property in the constructor. Pass in the DAO
12 | // instead of the whole database, because you only need access to the DAO
13 | class CredentialsRepository(val moshi: Moshi) {
14 |
15 | fun getCredentials(): Credentials? {
16 | // read the file credentials.json with moshi
17 | val bufferedReader: BufferedReader = File(CREDENTIALS_PATH).bufferedReader()
18 | val credentials = bufferedReader.use { it.readText() }
19 | val adapter: JsonAdapter = moshi.adapter(Credentials::class.java)
20 | return adapter.fromJson(credentials)
21 | }
22 |
23 | fun insertCredentials(
24 | deviceCode: String,
25 | clientId: String?,
26 | clientSecret: String?,
27 | accessToken: String?,
28 | refreshToken: String?
29 | ) {
30 | val adapter: JsonAdapter = moshi.adapter(Credentials::class.java)
31 | val credentials = Credentials(deviceCode, clientId, clientSecret, accessToken, refreshToken)
32 | // val jsonWriter: JsonWriter = JsonWriter.of(Paths.get(CREDENTIALS_PATH).toFile().sink().buffer())
33 | val json = adapter.toJson(credentials)
34 | File(CREDENTIALS_PATH).writeText(json)
35 | }
36 |
37 | fun insertPrivateToken(accessToken: String) {
38 | insertCredentials(PRIVATE_TOKEN, PRIVATE_TOKEN, PRIVATE_TOKEN, accessToken, PRIVATE_TOKEN)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/DownloadRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import com.github.livingwithhippos.unchained_bot.data.remote.DownloadApiHelper
5 |
6 | class DownloadRepository(private val downloadApiHelper: DownloadApiHelper) :
7 | BaseRepository() {
8 | suspend fun getDownloads(
9 | token: String,
10 | offset: Int? = 0,
11 | page: Int = 1,
12 | limit: Int = 50
13 | ): List {
14 |
15 | val downloadResponse = safeApiCall(
16 | call = { downloadApiHelper.getDownloads("Bearer $token", offset, page, limit) },
17 | errorMessage = "Error Fetching User Info"
18 | )
19 |
20 | return downloadResponse ?: emptyList()
21 | }
22 |
23 | suspend fun deleteDownload(token: String, id: String): Unit? {
24 |
25 | val response = safeApiCall(
26 | call = {
27 | downloadApiHelper.deleteDownload(
28 | token = "Bearer $token",
29 | id = id
30 | )
31 | },
32 | errorMessage = "Error deleting download"
33 | )
34 |
35 | return response
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/StreamingRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.Stream
4 | import com.github.livingwithhippos.unchained_bot.data.remote.StreamingApiHelper
5 |
6 | class StreamingRepository(private val streamingApiHelper: StreamingApiHelper) :
7 | BaseRepository() {
8 |
9 | suspend fun getStreams(token: String, id: String): Stream? {
10 |
11 | val streamResponse = safeApiCall(
12 | call = { streamingApiHelper.getStreams("Bearer $token", id) },
13 | errorMessage = "Error Fetching Streaming Info"
14 | )
15 |
16 | return streamResponse
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/TorrentsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.AvailableHost
4 | import com.github.livingwithhippos.unchained_bot.data.model.TorrentItem
5 | import com.github.livingwithhippos.unchained_bot.data.model.UploadedTorrent
6 | import com.github.livingwithhippos.unchained_bot.data.remote.TorrentApiHelper
7 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
8 | import okhttp3.RequestBody
9 | import okhttp3.RequestBody.Companion.toRequestBody
10 |
11 | class TorrentsRepository(private val torrentApiHelper: TorrentApiHelper) :
12 | BaseRepository() {
13 |
14 | suspend fun getAvailableHosts(token: String): List? {
15 | val hostResponse: List? = safeApiCall(
16 | call = { torrentApiHelper.getAvailableHosts(token = "Bearer $token") },
17 | errorMessage = "Error Retrieving Available Hosts"
18 | )
19 |
20 | return hostResponse
21 | }
22 |
23 | suspend fun getTorrentInfo(
24 | token: String,
25 | id: String
26 | ): TorrentItem? {
27 | val torrentResponse: TorrentItem? = safeApiCall(
28 | call = {
29 | torrentApiHelper.getTorrentInfo(
30 | token = "Bearer $token",
31 | id = id
32 | )
33 | },
34 | errorMessage = "Error Retrieving Torrent Info"
35 | )
36 |
37 | return torrentResponse
38 | }
39 |
40 | suspend fun addTorrent(
41 | token: String,
42 | binaryTorrent: ByteArray,
43 | host: String
44 | ): UploadedTorrent? {
45 |
46 | val requestBody: RequestBody = binaryTorrent.toRequestBody(
47 | "application/octet-stream".toMediaTypeOrNull(),
48 | 0,
49 | binaryTorrent.size
50 | )
51 |
52 | val addTorrentResponse = safeApiCall(
53 | call = {
54 | torrentApiHelper.addTorrent(
55 | token = "Bearer $token",
56 | binaryTorrent = requestBody,
57 | host = host
58 | )
59 | },
60 | errorMessage = "Error Uploading Torrent"
61 | )
62 |
63 | return addTorrentResponse
64 | }
65 |
66 | suspend fun addMagnet(
67 | token: String,
68 | magnet: String,
69 | host: String
70 | ): UploadedTorrent? {
71 | val torrentResponse = safeApiCall(
72 | call = {
73 | torrentApiHelper.addMagnet(
74 | token = "Bearer $token",
75 | magnet = magnet,
76 | host = host
77 | )
78 | },
79 | errorMessage = "Error Uploading Torrent From Magnet"
80 | )
81 |
82 | return torrentResponse
83 | }
84 |
85 | suspend fun getTorrentsList(
86 | token: String,
87 | offset: Int? = null,
88 | page: Int? = 1,
89 | limit: Int? = 50,
90 | filter: String? = null
91 | ): List {
92 |
93 | val torrentsResponse: List? = safeApiCall(
94 | call = {
95 | torrentApiHelper.getTorrentsList(
96 | token = "Bearer $token",
97 | offset = offset,
98 | page = page,
99 | limit = limit,
100 | filter = filter
101 | )
102 | },
103 | errorMessage = "Error Retrieving Torrent Info"
104 | )
105 |
106 | return torrentsResponse ?: emptyList()
107 | }
108 |
109 | suspend fun selectFiles(
110 | token: String,
111 | id: String,
112 | files: String = "all"
113 | ) {
114 |
115 | // this call has no return type
116 | safeApiCall(
117 | call = {
118 | torrentApiHelper.selectFiles(
119 | token = "Bearer $token",
120 | id = id,
121 | files = files
122 | )
123 | },
124 | errorMessage = "Error Selecting Torrent Files"
125 | )
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/UnrestrictRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.DownloadItem
4 | import com.github.livingwithhippos.unchained_bot.data.remote.UnrestrictApiHelper
5 |
6 | class UnrestrictRepository(private val unrestrictApiHelper: UnrestrictApiHelper) :
7 | BaseRepository() {
8 |
9 | suspend fun getUnrestrictedLink(
10 | token: String,
11 | link: String,
12 | password: String? = null,
13 | remote: Int? = null
14 | ): DownloadItem? {
15 |
16 | return safeApiCall(
17 | call = {
18 | unrestrictApiHelper.getUnrestrictedLink(
19 | token = "Bearer $token",
20 | link = link,
21 | password = password,
22 | remote = remote
23 | )
24 | },
25 | errorMessage = "Error Fetching Unrestricted Link Info"
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.model.User
4 | import com.github.livingwithhippos.unchained_bot.data.remote.UserApiHelper
5 |
6 | class UserRepository(private val userApiHelper: UserApiHelper) :
7 | BaseRepository() {
8 |
9 | suspend fun getUserInfo(token: String): User? {
10 |
11 | val userResponse = safeApiCall(
12 | call = { userApiHelper.getUser("Bearer $token") },
13 | errorMessage = "Error Fetching User Info"
14 | )
15 |
16 | return userResponse
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/data/repository/VariousApiRepository.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.data.repository
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.remote.VariousApiHelper
4 |
5 | class VariousApiRepository(private val variousApiHelper: VariousApiHelper) :
6 | BaseRepository() {
7 |
8 | suspend fun disableToken(token: String): Unit? {
9 |
10 | val response = safeApiCall(
11 | call = {
12 | variousApiHelper.disableToken(
13 | token = "Bearer $token"
14 | )
15 | },
16 | errorMessage = "Error disabling token"
17 | )
18 |
19 | return response
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/di/ApiFactory.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.di
2 |
3 | import com.github.livingwithhippos.unchained_bot.BASE_AUTH_URL
4 | import com.github.livingwithhippos.unchained_bot.BASE_URL
5 | import com.github.livingwithhippos.unchained_bot.data.remote.DownloadsApi
6 | import com.github.livingwithhippos.unchained_bot.data.remote.StreamingApi
7 | import com.github.livingwithhippos.unchained_bot.data.remote.TorrentsApi
8 | import com.github.livingwithhippos.unchained_bot.data.remote.UnrestrictApi
9 | import com.github.livingwithhippos.unchained_bot.data.remote.UserApi
10 | import com.squareup.moshi.Moshi
11 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
12 | import okhttp3.OkHttpClient
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.moshi.MoshiConverterFactory
15 |
16 | /**
17 | * This object manages the Dagger-Hilt injection for the OkHttp and Retrofit clients
18 | */
19 |
20 | object ApiFactory {
21 |
22 | fun provideOkHttpClient(): OkHttpClient {
23 | // remove in production
24 | /*
25 | val logInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
26 | level = HttpLoggingInterceptor.Level.BODY
27 | }
28 | */
29 |
30 | return OkHttpClient().newBuilder()
31 | // logs all the calls, removed in the release channel
32 | // .addInterceptor(logInterceptor)
33 | // avoid issues with empty bodies on delete/put and 20x return codes
34 | // .addInterceptor(EmptyBodyInterceptor)
35 | .build()
36 | }
37 |
38 | fun authRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
39 | .client(okHttpClient)
40 | .baseUrl(BASE_AUTH_URL)
41 | .addConverterFactory(MoshiConverterFactory.create())
42 | .build()
43 |
44 | fun apiRetrofit(okHttpClient: OkHttpClient): Retrofit {
45 | val moshi = Moshi.Builder()
46 | .addLast(KotlinJsonAdapterFactory())
47 | .build()
48 |
49 | return Retrofit.Builder()
50 | .client(okHttpClient)
51 | .baseUrl(BASE_URL)
52 | .addConverterFactory(MoshiConverterFactory.create(moshi))
53 | .build()
54 | }
55 |
56 | fun provideUserApi(retrofit: Retrofit): UserApi {
57 | return retrofit.create(UserApi::class.java)
58 | }
59 |
60 | fun provideUnrestrictApi(retrofit: Retrofit): UnrestrictApi {
61 | return retrofit.create(UnrestrictApi::class.java)
62 | }
63 |
64 | fun provideStreamingApi(retrofit: Retrofit): StreamingApi {
65 | return retrofit.create(StreamingApi::class.java)
66 | }
67 |
68 | fun provideTorrentsApi(retrofit: Retrofit): TorrentsApi {
69 | return retrofit.create(TorrentsApi::class.java)
70 | }
71 |
72 | fun provideDownloadsApi(retrofit: Retrofit): DownloadsApi {
73 | return retrofit.create(DownloadsApi::class.java)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/di/KoinModule.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.di
2 |
3 | import com.github.livingwithhippos.unchained_bot.data.remote.DownloadApiHelper
4 | import com.github.livingwithhippos.unchained_bot.data.remote.DownloadApiHelperImpl
5 | import com.github.livingwithhippos.unchained_bot.data.remote.DownloadsApi
6 | import com.github.livingwithhippos.unchained_bot.data.remote.StreamingApi
7 | import com.github.livingwithhippos.unchained_bot.data.remote.StreamingApiHelper
8 | import com.github.livingwithhippos.unchained_bot.data.remote.StreamingApiHelperImpl
9 | import com.github.livingwithhippos.unchained_bot.data.remote.TorrentApiHelper
10 | import com.github.livingwithhippos.unchained_bot.data.remote.TorrentApiHelperImpl
11 | import com.github.livingwithhippos.unchained_bot.data.remote.TorrentsApi
12 | import com.github.livingwithhippos.unchained_bot.data.remote.UnrestrictApi
13 | import com.github.livingwithhippos.unchained_bot.data.remote.UnrestrictApiHelper
14 | import com.github.livingwithhippos.unchained_bot.data.remote.UnrestrictApiHelperImpl
15 | import com.github.livingwithhippos.unchained_bot.data.remote.UserApi
16 | import com.github.livingwithhippos.unchained_bot.data.remote.UserApiHelper
17 | import com.github.livingwithhippos.unchained_bot.data.remote.UserApiHelperImpl
18 | import com.github.livingwithhippos.unchained_bot.data.repository.CredentialsRepository
19 | import com.github.livingwithhippos.unchained_bot.data.repository.DownloadRepository
20 | import com.github.livingwithhippos.unchained_bot.data.repository.StreamingRepository
21 | import com.github.livingwithhippos.unchained_bot.data.repository.TorrentsRepository
22 | import com.github.livingwithhippos.unchained_bot.data.repository.UnrestrictRepository
23 | import com.github.livingwithhippos.unchained_bot.data.repository.UserRepository
24 | import com.squareup.moshi.Moshi
25 | import okhttp3.OkHttpClient
26 | import org.koin.dsl.module
27 | import retrofit2.Retrofit
28 |
29 | val myModules = module {
30 |
31 | single { MoshiAdapter.getBuilder() }
32 | single { CredentialsRepository(get()) }
33 | single { ApiFactory.provideOkHttpClient() }
34 | single { ApiFactory.apiRetrofit(get()) }
35 |
36 | single { ApiFactory.provideUserApi(get()) }
37 | single { UserApiHelperImpl(get()) }
38 | single { UserRepository(get()) }
39 |
40 | single { ApiFactory.provideUnrestrictApi(get()) }
41 | single { UnrestrictApiHelperImpl(get()) }
42 | single { UnrestrictRepository(get()) }
43 |
44 | single { ApiFactory.provideStreamingApi(get()) }
45 | single { StreamingApiHelperImpl(get()) }
46 | single { StreamingRepository(get()) }
47 |
48 | single { ApiFactory.provideTorrentsApi(get()) }
49 | single { TorrentApiHelperImpl(get()) }
50 | single { TorrentsRepository(get()) }
51 |
52 | single { ApiFactory.provideDownloadsApi(get()) }
53 | single { DownloadApiHelperImpl(get()) }
54 | single { DownloadRepository(get()) }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/di/MoshiAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.di
2 |
3 | import com.squareup.moshi.Moshi
4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
5 |
6 | object MoshiAdapter {
7 |
8 | fun getBuilder(): Moshi {
9 | // KotlinJsonAdapterFactory()
10 | return Moshi.Builder()
11 | .add(KotlinJsonAdapterFactory())
12 | .build()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/localization/EN.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.localization
2 |
3 | object EN : Localization {
4 | override val progress: String
5 | get() = "Progress"
6 | override val helpMessage: String
7 | get() = """
8 | *Command list:*
9 | /help - display the list of available commands
10 | /user - get Real Debrid user's information
11 | /torrents [number, default 5] - list the last torrents
12 | /downloads [number, default 5] - list the last downloads
13 | /get [unrestricted link] - downloads the link on the directory of the server running the bot
14 | /unrestrict [url|magnet|torrent file link] - generate a download link. Magnet/Torrents will be queued, check their status with /torrents
15 | /transcode [real debrid file id] - transcode streaming links to various quality levels. Get the file id using /unrestrict
16 | """.trimIndent()
17 | override val privateKeyError: String
18 | get() = "Couldn't load your real debrid data\nCheck your private api key."
19 | override val botStarted: String
20 | get() = "Bot started"
21 | override val name: String
22 | get() = "Name"
23 | override val size: String
24 | get() = "Size"
25 | override val link: String
26 | get() = "Link"
27 | override val transcodingInstructions: String
28 | get() = "Streaming transcoding with"
29 | override val streamingUnavailable: String
30 | get() = "Streaming not available"
31 | override val id: String
32 | get() = "id"
33 | override val username: String
34 | get() = "username"
35 | override val email: String
36 | get() = "email"
37 | override val points: String
38 | get() = "points"
39 | override val status: String
40 | get() = "status"
41 | override val premium: String
42 | get() = "premium"
43 | override val days: String
44 | get() = "days"
45 | override val expiration: String
46 | get() = "expiration"
47 | override val welcomeMessage: String
48 | get() = "Welcome back, %username%.\nYou have %days% days of premium\nand %points% points remaining."
49 | override val unrestrict: String
50 | get() = "Unrestrict"
51 | override val unrestrictDescription: String
52 | get() = "Unrestrict a single link"
53 | override val unrestrictError: String
54 | get() = "Error unrestrcting link. Unsupported host?"
55 | override val startingDownload: String
56 | get() = "Starting download"
57 | override val getDownloadLink: String
58 | get() = "Get the download link with"
59 | override val wrongDownloadSyntax: String
60 | get() = "Wrong or missing argument.\nSyntax: /get [unrestricted link]"
61 | override val wrongStreamSyntax: String
62 | get() = "Wrong or missing argument.\nSyntax: /stream [real debrid file id]"
63 | override val wrongUnrestrictSyntax: String
64 | get() = "Wrong or missing argument.\nSyntax: /unrestrict [url|magnet|torrent file link]"
65 | override val addedTorrent: String
66 | get() = "Added torrent with id %id%, check its status with /torrents"
67 | override val uploadingTorrent: String
68 | get() = "Uploading torrent to Real Debrid. Check its status with /torrents"
69 | override val appleQuality: String
70 | get() = "Apple quality"
71 | override val dashQuality: String
72 | get() = "Dash quality"
73 | override val liveMP4Quality: String
74 | get() = "liveMp4 quality"
75 | override val h264WebMQuality: String
76 | get() = "h264WebM quality"
77 | override val error: String
78 | get() = "Error"
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/localization/IT.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.localization
2 |
3 | object IT : Localization {
4 | override val progress: String
5 | get() = "Progresso"
6 | override val helpMessage: String
7 | get() = """
8 | *Lista comandi:*
9 | /help - mostra la lista di comandi disponibili
10 | /user - mostra le informazioni utente
11 | /torrents \[numero, default 5] - mostra la lista degli ultimi n torrent
12 | /downloads \[numero, default 5] - mostra la lista degli ultimi n download
13 | /get \[url] - scarica un link nel server in cui sta girando il bot
14 | /unrestrict \[url|magnet|link file torrent] - genera un link di download. Magnet/Torrents verranno messi in coda, controlla il loro status con /torrents
15 | /transcode \[id file real debrid] - offre codifiche per streaming di varie qualità. Ottieni l'id con /unrestrict
16 | """.trimIndent()
17 | override val privateKeyError: String
18 | get() = "Impossibile caricare i dati da Real Debrid.\nControlla la tua chiave API."
19 | override val botStarted: String
20 | get() = "Bot avviato"
21 | override val name: String
22 | get() = "Nome"
23 | override val size: String
24 | get() = "Dimensioni"
25 | override val link: String
26 | get() = "Link"
27 | override val transcodingInstructions: String
28 | get() = "Codifica streaming con "
29 | override val streamingUnavailable: String
30 | get() = "Streaming non disponibile"
31 | override val id: String
32 | get() = "id"
33 | override val username: String
34 | get() = "nome utente"
35 | override val email: String
36 | get() = "email"
37 | override val points: String
38 | get() = "punti"
39 | override val status: String
40 | get() = "stato"
41 | override val premium: String
42 | get() = "premium"
43 | override val days: String
44 | get() = "giorni"
45 | override val expiration: String
46 | get() = "scadenza"
47 | override val welcomeMessage: String
48 | get() = "Bentornato, %username%.\nHai %days% giorni di servizio premium\ne %points% punti rimanenti."
49 | override val unrestrict: String
50 | get() = "Sblocca"
51 | override val unrestrictDescription: String
52 | get() = "Sblocca un singolo link"
53 | override val unrestrictError: String
54 | get() = "Errore di sblocco link. Host non supportato?"
55 | override val startingDownload: String
56 | get() = "Avviando il download"
57 | override val getDownloadLink: String
58 | get() = "Ottieni link per download con"
59 | override val wrongDownloadSyntax: String
60 | get() = "Parametro errato o mancante.\nSintassi: /get [link sbloccato]"
61 | override val wrongStreamSyntax: String
62 | get() = "Parametro errato o mancante.\nSintassi: /stream [id file real debrid]"
63 | override val wrongUnrestrictSyntax: String
64 | get() = "Parametro errato o mancante.\nSintassi: /unrestrict [url|magnet|link file torrent]"
65 | override val addedTorrent: String
66 | get() = "Aggiunto torrent con id %id%, controlla il suo stato con /torrents"
67 | override val uploadingTorrent: String
68 | get() = "Caricando torrent su Real Debrid, controlla il suo stato con /torrents"
69 | override val appleQuality: String
70 | get() = "Qualità Apple"
71 | override val dashQuality: String
72 | get() = "Qualità Dash"
73 | override val liveMP4Quality: String
74 | get() = "Qualità liveMp4"
75 | override val h264WebMQuality: String
76 | get() = "Qualità h264WebM"
77 | override val error: String
78 | get() = "Errore"
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/localization/Localization.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.localization
2 |
3 | interface Localization {
4 | val progress: String
5 | val helpMessage: String
6 | val privateKeyError: String
7 | val botStarted: String
8 | val name: String
9 | val size: String
10 | val link: String
11 | val transcodingInstructions: String
12 | val streamingUnavailable: String
13 | val id: String
14 | val username: String
15 | val email: String
16 | val points: String
17 | val status: String
18 | val premium: String
19 | val days: String
20 | val expiration: String
21 | val welcomeMessage: String
22 | val unrestrict: String
23 | val unrestrictDescription: String
24 | val unrestrictError: String
25 | val startingDownload: String
26 | val getDownloadLink: String
27 | val wrongDownloadSyntax: String
28 | val wrongStreamSyntax: String
29 | val wrongUnrestrictSyntax: String
30 | val addedTorrent: String
31 | val uploadingTorrent: String
32 | val appleQuality: String
33 | val dashQuality: String
34 | val liveMP4Quality: String
35 | val h264WebMQuality: String
36 | val error: String
37 | }
38 |
39 | val localeMapping: Map = mapOf(
40 | Pair("en", EN),
41 | Pair("it", IT)
42 | )
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/utilities/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.utilities
2 |
3 | import com.github.livingwithhippos.unchained_bot.MAGNET_PATTERN
4 | import com.github.livingwithhippos.unchained_bot.TORRENT_PATTERN
5 | import java.io.File
6 | import java.io.IOException
7 | import java.util.concurrent.TimeUnit
8 | import java.util.regex.Matcher
9 | import java.util.regex.Pattern
10 |
11 | /**
12 | * check if a String is an url
13 | */
14 |
15 | fun String.isWebUrl(): Boolean = Patterns.WEB_URL.matcher(this).matches()
16 |
17 | /**
18 | * check if a String is a magnet link
19 | */
20 | fun String?.isMagnet(): Boolean {
21 | if (this == null)
22 | return false
23 | val m: Matcher = Pattern.compile(MAGNET_PATTERN).matcher(this)
24 | return m.lookingAt()
25 | }
26 |
27 | /**
28 | * check if a String is a torrent link
29 | */
30 | fun String?.isTorrent(): Boolean {
31 | if (this == null)
32 | return false
33 | val m: Matcher = Pattern.compile(TORRENT_PATTERN).matcher(this)
34 | return m.matches()
35 | }
36 |
37 | /**
38 | * execute a string as a command in the shell. Redirect output to stderr
39 | */
40 | fun String.runCommand(workingDir: File? = null) {
41 | val process = ProcessBuilder(*split(" ").toTypedArray())
42 | .directory(workingDir)
43 | .redirectOutput(ProcessBuilder.Redirect.INHERIT)
44 | .redirectError(ProcessBuilder.Redirect.INHERIT)
45 | .start()
46 | // unnecessary for a download manager
47 | /*
48 | if (!process.waitFor(10, TimeUnit.SECONDS)) {
49 | process.destroy()
50 | throw RuntimeException("execution timed out: $this")
51 | }
52 | if (process.exitValue() != 0) {
53 | // we don't care if the download manager has issues
54 | throw RuntimeException("execution failed with code ${process.exitValue()}: $this")
55 | //println("execution failed with code ${process.exitValue()}: $this")
56 | }
57 | */
58 | }
59 |
60 | /**
61 | * execute a string as a command in the shell. Redirect output to a String
62 | */
63 | fun String.runCommandWithOutput(workingDir: File): String? {
64 | try {
65 | val parts = this.split("\\s".toRegex())
66 | val proc = ProcessBuilder(*parts.toTypedArray())
67 | .directory(workingDir)
68 | .redirectOutput(ProcessBuilder.Redirect.PIPE)
69 | .redirectError(ProcessBuilder.Redirect.PIPE)
70 | .start()
71 |
72 | proc.waitFor(60, TimeUnit.MINUTES)
73 | return proc.inputStream.bufferedReader().readText()
74 | } catch (e: IOException) {
75 | e.printStackTrace()
76 | return null
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/com/github/livingwithhippos/unchained_bot/utilities/Patterns.kt:
--------------------------------------------------------------------------------
1 | package com.github.livingwithhippos.unchained_bot.utilities
2 |
3 | import java.util.regex.Pattern
4 |
5 | /*
6 | * Copyright (C) 2007 The Android Open Source Project
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | */
20 |
21 | /**
22 | * This file has been modified. Mainly ported to Kotlin to be used outside the android environment.
23 | */
24 |
25 | /**
26 | * Commonly used regular expression patterns.
27 | */
28 | object Patterns {
29 |
30 | /**
31 | * Regular expression to match all IANA top-level domains.
32 | *
33 | * List accurate as of 2015/11/24. List taken from:
34 | * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
35 | * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
36 | *
37 | * @hide
38 | */
39 | private const val IANA_TOP_LEVEL_DOMAINS = (
40 | "(?:" +
41 | "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active" +
42 | "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica|amsterdam" +
43 | "|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia|associates" +
44 | "|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])" +
45 | "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva" +
46 | "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black" +
47 | "|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots|boutique" +
48 | "|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build|builders|business" +
49 | "|buzz|bzh|b[abdefghijmnorstvwyz])" +
50 | "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards" +
51 | "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center|ceo" +
52 | "|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani|cisco" +
53 | "|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed|coach" +
54 | "|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec|condos" +
55 | "|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses" +
56 | "|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])" +
57 | "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta" +
58 | "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount" +
59 | "|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])" +
60 | "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises" +
61 | "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert|exposed" +
62 | "|express|e[cegrstu])" +
63 | "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|feedback|ferrero|film" +
64 | "|final|finance|financial|firmdale|fish|fishing|fit|fitness|flights|florist|flowers|flsmidth" +
65 | "|fly|foo|football|forex|forsale|forum|foundation|frl|frogans|fund|furniture|futbol|fyi" +
66 | "|f[ijkmor])" +
67 | "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving" +
68 | "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov|grainger" +
69 | "|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])" +
70 | "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey|holdings" +
71 | "|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hsbc|hyundai" +
72 | "|h[kmnrtu])" +
73 | "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink|institute" +
74 | "|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau|iwc|i[delmnoqrst])" +
75 | "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])" +
76 | "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto|k[eghimnprwyz])" +
77 | "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease|leclerc" +
78 | "|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde|link|live" +
79 | "|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury|l[abcikrstuvy])" +
80 | "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba" +
81 | "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi|moda" +
82 | "|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar" +
83 | "|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])" +
84 | "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk" +
85 | "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])" +
86 | "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka" +
87 | "|otsuka|ovh|om)" +
88 | "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography" +
89 | "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation|plumbing" +
90 | "|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties|property" +
91 | "|protection|pub|p[aefghklmnrstwy])" +
92 | "|(?:qpon|quebec|qa)" +
93 | "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals" +
94 | "|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip|rocher|rocks" +
95 | "|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])" +
96 | "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo" +
97 | "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat|security" +
98 | "|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles|site|ski" +
99 | "|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting" +
100 | "|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study|style|sucks|supplies" +
101 | "|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems|s[abcdeghijklmnortuvxyz])" +
102 | "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica" +
103 | "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo|tools" +
104 | "|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust|tui|t[cdfghjklmnortvwz])" +
105 | "|(?:ubs|university|uno|uol|u[agksyz])" +
106 | "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin" +
107 | "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])" +
108 | "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki|williamhill" +
109 | "|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])" +
110 | "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c|\u043c\u043a\u0434" +
111 | "|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d" +
112 | "|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431" +
113 | "|\u0443\u043a\u0440|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0627\u0631\u0627\u0645\u0643\u0648" +
114 | "|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629" +
115 | "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0627\u06cc\u0631\u0627\u0646" +
116 | "|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633" +
117 | "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0634\u0628\u0643\u0629" +
118 | "|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0641\u0644\u0633\u0637\u064a\u0646" +
119 | "|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627" +
120 | "|\u0645\u0648\u0642\u0639|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924" +
121 | "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4" +
122 | "|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd" +
123 | "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22" +
124 | "|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb|\u30b3\u30e0|\u4e16\u754c" +
125 | "|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51|\u4f01\u4e1a|\u4f5b\u5c71" +
126 | "|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063" +
127 | "|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c" +
128 | "|\u5e7f\u4e1c|\u6148\u5584|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c" +
129 | "|\u65b0\u52a0\u5761|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f" +
130 | "|\u70b9\u770b|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc" +
131 | "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137" +
132 | "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox" +
133 | "|xerox|xin|xn\\-\\-11b4c3d|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g" +
134 | "|xn\\-\\-3e0b707e|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim" +
135 | "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks" +
136 | "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais|xn\\-\\-9dbq2a" +
137 | "|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd" +
138 | "|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h" +
139 | "|xn\\-\\-estv75g|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s" +
140 | "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c" +
141 | "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i" +
142 | "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d" +
143 | "|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt" +
144 | "|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e" +
145 | "|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab" +
146 | "|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema" +
147 | "|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh" +
148 | "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-s9brj9c" +
149 | "|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb" +
150 | "|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a" +
151 | "|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o" +
152 | "|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xperia|xxx|xyz)" +
153 | "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])" +
154 | "|(?:zara|zip|zone|zuerich|z[amw]))"
155 | )
156 |
157 | private const val IP_ADDRESS_STRING = (
158 | "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" +
159 | "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" +
160 | "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" +
161 | "|[1-9][0-9]|[0-9]))"
162 | )
163 |
164 | /**
165 | * Valid UCS characters defined in RFC 3987. Excludes space characters.
166 | */
167 | private const val UCS_CHAR = "[" +
168 | "\u00A0-\uD7FF" +
169 | "\uF900-\uFDCF" +
170 | "\uFDF0-\uFFEF" +
171 | "\uD800\uDC00-\uD83F\uDFFD" +
172 | "\uD840\uDC00-\uD87F\uDFFD" +
173 | "\uD880\uDC00-\uD8BF\uDFFD" +
174 | "\uD8C0\uDC00-\uD8FF\uDFFD" +
175 | "\uD900\uDC00-\uD93F\uDFFD" +
176 | "\uD940\uDC00-\uD97F\uDFFD" +
177 | "\uD980\uDC00-\uD9BF\uDFFD" +
178 | "\uD9C0\uDC00-\uD9FF\uDFFD" +
179 | "\uDA00\uDC00-\uDA3F\uDFFD" +
180 | "\uDA40\uDC00-\uDA7F\uDFFD" +
181 | "\uDA80\uDC00-\uDABF\uDFFD" +
182 | "\uDAC0\uDC00-\uDAFF\uDFFD" +
183 | "\uDB00\uDC00-\uDB3F\uDFFD" +
184 | "\uDB44\uDC00-\uDB7F\uDFFD" +
185 | "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]"
186 |
187 | /**
188 | * Valid characters for IRI label defined in RFC 3987.
189 | */
190 | private val LABEL_CHAR = "a-zA-Z0-9$UCS_CHAR"
191 |
192 | /**
193 | * Valid characters for IRI TLD defined in RFC 3987.
194 | */
195 | private val TLD_CHAR = "a-zA-Z$UCS_CHAR"
196 |
197 | /**
198 | * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
199 | */
200 | private val IRI_LABEL = "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"
201 |
202 | /**
203 | * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters.
204 | */
205 | private const val PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w"
206 | private val TLD = "($PUNYCODE_TLD|[$TLD_CHAR]{2,63})"
207 | private val HOST_NAME = "($IRI_LABEL\\.)+$TLD"
208 | private val DOMAIN_NAME_STR = "($HOST_NAME|$IP_ADDRESS_STRING)"
209 | private const val PROTOCOL = "(?i:http|https|rtsp)://"
210 |
211 | /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
212 | private const val WORD_BOUNDARY = "(?:\\b|$|^)"
213 | private const val USER_INFO = (
214 | "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" +
215 | "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" +
216 | "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@"
217 | )
218 | private const val PORT_NUMBER = "\\:\\d{1,5}"
219 | private val PATH_AND_QUERY = (
220 | "[/\\?](?:(?:[" + LABEL_CHAR +
221 | ";/\\?:@&=#~" + // plus optional query params
222 | "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*"
223 | )
224 |
225 | /**
226 | * Regular expression pattern to match most part of RFC 3987
227 | * Internationalized URLs, aka IRIs.
228 | */
229 | val WEB_URL: Pattern = Pattern.compile(
230 | "(" +
231 | "(" +
232 | "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?" +
233 | "(?:" + DOMAIN_NAME_STR + ")" +
234 | "(?:" + PORT_NUMBER + ")?" +
235 | ")" +
236 | "(" + PATH_AND_QUERY + ")?" +
237 | WORD_BOUNDARY +
238 | ")"
239 | )
240 |
241 | /**
242 | * Regular expression that matches known TLDs and punycode TLDs
243 | */
244 | private val STRICT_TLD = (
245 | "(?:" +
246 | IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")"
247 | )
248 |
249 | /**
250 | * Regular expression that matches host names using [.STRICT_TLD]
251 | */
252 | private val STRICT_HOST_NAME = (
253 | "(?:(?:" + IRI_LABEL + "\\.)+" +
254 | STRICT_TLD + ")"
255 | )
256 |
257 | /**
258 | * Regular expression that matches domain names using either [.STRICT_HOST_NAME] or
259 | * [.IP_ADDRESS]
260 | */
261 | private val STRICT_DOMAIN_NAME = (
262 | "(?:" + STRICT_HOST_NAME + "|" +
263 | IP_ADDRESS_STRING + ")"
264 | )
265 |
266 | /**
267 | * Regular expression that matches domain names without a TLD
268 | */
269 | private val RELAXED_DOMAIN_NAME =
270 | "(?:(?:$IRI_LABEL(?:\\.(?=\\S))?)+|$IP_ADDRESS_STRING)"
271 |
272 | /**
273 | * Regular expression to match strings that do not start with a supported protocol. The TLDs
274 | * are expected to be one of the known TLDs.
275 | */
276 | private val WEB_URL_WITHOUT_PROTOCOL = (
277 | "(" +
278 | WORD_BOUNDARY +
279 | "(?