From 1310afdaf0e7fd39e3e59d8095e15d09f3b978e3 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Thu, 22 Aug 2024 11:30:57 +0330 Subject: [PATCH 001/110] Remove chain from GLMStaking params --- core/constraints/__init__.py | 2 +- core/constraints/abstract.py | 2 +- .../constraints/{glm_staking.py => octant.py} | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) rename core/constraints/{glm_staking.py => octant.py} (76%) diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index 7e599bc..06bd36c 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -38,7 +38,6 @@ HasGitcoinPassportProfile, HasMinimumHumanityScore, ) -from core.constraints.glm_staking import GLMStakingVerification from core.constraints.lens import ( BeFollowedByLensUser, DidCollectLensPublication, @@ -49,6 +48,7 @@ IsFollowingLensUser, ) from core.constraints.muon_node import HasMuonNode +from core.constraints.octant import GLMStakingVerification from core.constraints.optimism import DelegateOP, DidDelegateOPToAddress from core.constraints.twitter import ( BeFollowedByTwitterUser, diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index a47bce9..d4c596f 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -15,7 +15,7 @@ class ConstraintApp(Enum): TWITTER = "twitter" MUON = "muon" OPTIMISM = "optimism" - GLM_STAKING = "glm_staking" + OCTANT = "octant" @classmethod def choices(cls): diff --git a/core/constraints/glm_staking.py b/core/constraints/octant.py similarity index 76% rename from core/constraints/glm_staking.py rename to core/constraints/octant.py index 3aeb02f..bf37732 100644 --- a/core/constraints/glm_staking.py +++ b/core/constraints/octant.py @@ -1,12 +1,16 @@ -from core.constraints.abstract import ConstraintParam, ConstraintVerification, ConstraintApp -from core.utils import Web3Utils, InvalidAddressException from rest_framework.exceptions import ValidationError -from core.constants import GLM_ABI +from core.constants import GLM_ABI +from core.constraints.abstract import ( + ConstraintApp, + ConstraintParam, + ConstraintVerification, +) +from core.utils import InvalidAddressException, Web3Utils class GLMStakingVerification(ConstraintVerification): - app_name = ConstraintApp.GLM_STAKING.value + app_name = ConstraintApp.OCTANT.value _param_keys = [ ConstraintParam.CHAIN, @@ -20,26 +24,22 @@ def __init__(self, user_profile) -> None: def is_observed(self, *args, **kwargs): from core.models import Chain - chain_pk = self.param_values[ConstraintParam.CHAIN.name] - minimum = self.param_values[ConstraintParam.MINIMUM.name] - chain = Chain.objects.get(pk=chain_pk) + chain = Chain.objects.get(chain_id="1") web3_utils = Web3Utils(chain.rpc_url_private, chain.poa) - staked_amount = 0 try: for wallet in self.user_addresses: - staked_amount += self.get_staked_amount(wallet, web3_utils) - + staked_amount += self.get_staked_amount(wallet, web3_utils) + except InvalidAddressException as e: raise ValidationError({"address": str(e)}) - + return staked_amount >= minimum def get_staked_amount(self, user_address: str, web3_utils: Web3Utils) -> int: web3_utils.set_contract(self.GLM_CONTRACT_ADDRESS, GLM_ABI) deposits_function = web3_utils.contract.functions.deposits(user_address) return web3_utils.contract_call(deposits_function) - From f42ab878ada8a338488f7d7dc1319aacb4144a2f Mon Sep 17 00:00:00 2001 From: Shayan Shiravani Date: Thu, 22 Aug 2024 16:36:12 +0330 Subject: [PATCH 002/110] gitcoin donation constraint --- core/constraints/__init__.py | 1 + core/constraints/abstract.py | 1 + core/constraints/gitcoin_passport.py | 84 +++++++++++++++++++ core/models.py | 2 + core/thirdpartyapp/subgraph.py | 13 ++- .../migrations/0075_alter_constraint_name.py | 18 ++++ .../migrations/0061_alter_constraint_name.py | 18 ++++ 7 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 prizetap/migrations/0075_alter_constraint_name.py create mode 100644 tokenTap/migrations/0061_alter_constraint_name.py diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index 7e599bc..59fc67a 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -35,6 +35,7 @@ HasTokenVerification, ) from core.constraints.gitcoin_passport import ( + HasDonatedOnGitcoin, HasGitcoinPassportProfile, HasMinimumHumanityScore, ) diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index a47bce9..4f33d8d 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -44,6 +44,7 @@ class ConstraintParam(Enum): TWITTER_USERNAME = "twitter_username" TWITTER_IDS = "twitter_ids" FARCASTER_FIDS = "farcaster_fids" + COUNT = "count" @classmethod def choices(cls): diff --git a/core/constraints/gitcoin_passport.py b/core/constraints/gitcoin_passport.py index e8369dc..49ba054 100644 --- a/core/constraints/gitcoin_passport.py +++ b/core/constraints/gitcoin_passport.py @@ -1,9 +1,15 @@ +from enum import Enum + +from django.db.models.functions import Lower + from core.constraints.abstract import ( ConstraintApp, ConstraintParam, ConstraintVerification, ) +from ..thirdpartyapp.subgraph import Subgraph + class HasGitcoinPassportProfile(ConstraintVerification): _param_keys = [] @@ -49,3 +55,81 @@ def is_observed(self, *args, **kwargs) -> bool: ): return True return False + + +class HasDonatedParam(Enum): + MINIMUM = ConstraintParam.MINIMUM.name + COUNT = ConstraintParam.COUNT.name + ROUND = "round" + + +class HasDonatedOnGitcoin(ConstraintVerification): + _param_keys = [ + HasDonatedParam.MINIMUM, + HasDonatedParam.COUNT, + HasDonatedParam.ROUND, + ] + app_name = ConstraintApp.GITCOIN_PASSPORT.value + _graph_url = "https://grants-stack-indexer-v2.gitcoin.co" + + def __init__(self, user_profile) -> None: + super().__init__(user_profile) + + def is_observed(self, *args, **kwargs) -> bool: + try: + return self.has_donated( + self.param_values[HasDonatedParam.MINIMUM.name], + self.param_values[HasDonatedParam.COUNT.name], + self.param_values[HasDonatedParam.ROUND.name], + ) + except Exception as e: + print(e) + return False + + def has_donated(self, min, num_of_projects, round): + subgraph = Subgraph(self._graph_url) + + user_wallets = self.user_profile.wallets.values_list( + Lower("address"), flat=True + ) + + query = """ + query getDonationsByDonorAddress($addresses: [String!]!, $round: Int!) { + donations( + filter: { + donorAddress: {in: $addresses} + roundId: { equalTo: $round } + } + ) { + id + amountInUsd + projectId + } + } + """ + vars = {"addresses": list(user_wallets), "round": round} + + res = subgraph.send_post_request( + subgraph.path.get("gitcoin"), + query=query, + vars=vars, + other_json_params={"operationName": "getDonationsByDonorAddress"}, + ) + match res: + case {"data": {"donations": donations}} if len(donations) > 0: + donated_projects = [] + total_donation_amount = 0 + projects_count = 0 + for donation in donations: + total_donation_amount += donation["amountInUsd"] + if donation["projectId"] not in donated_projects: + donated_projects.append(donation["projectId"]) + projects_count += 1 + if ( + total_donation_amount > float(min) + and projects_count >= num_of_projects + ): + return True + case _: + return False + return False diff --git a/core/models.py b/core/models.py index 1f552bf..99b7070 100644 --- a/core/models.py +++ b/core/models.py @@ -32,6 +32,7 @@ DidRecastFarcasterCast, DidRetweetTweet, GLMStakingVerification, + HasDonatedOnGitcoin, HasENSVerification, HasFarcasterProfile, HasGitcoinPassportProfile, @@ -137,6 +138,7 @@ class Type(models.TextChoices): HasFarcasterProfile, BeAttestedBy, Attest, + HasDonatedOnGitcoin, HasMinimumHumanityScore, HasGitcoinPassportProfile, IsFollowingFarcasterChannel, diff --git a/core/thirdpartyapp/subgraph.py b/core/thirdpartyapp/subgraph.py index 64ed81c..fda90df 100644 --- a/core/thirdpartyapp/subgraph.py +++ b/core/thirdpartyapp/subgraph.py @@ -6,23 +6,28 @@ class Subgraph: - requests = RequestHelper(config.SUBGRAPH_BASE_URL) path = { "unitap_pass": "query/73675/unitap-pass-eth/version/latest", "arb_bridge_mainnet": "query/21879/unitap-arb-bridge-mainnet/version/latest", + "gitcoin": "graphql", } - def __init__(self): + def __init__(self, base_url=None): + self.requests = ( + RequestHelper(base_url) + if base_url + else RequestHelper(config.SUBGRAPH_BASE_URL) + ) self.session = self.requests.get_session() def __del__(self): self.session.close() - def send_post_request(self, path, query, vars, **kwargs): + def send_post_request(self, path, query, vars, other_json_params=None, **kwargs): try: return self.requests.post( path=path, - json={"query": query, "variables": vars}, + json={"query": query, "variables": vars, **other_json_params}, session=self.session, **kwargs, ) diff --git a/prizetap/migrations/0075_alter_constraint_name.py b/prizetap/migrations/0075_alter_constraint_name.py new file mode 100644 index 0000000..d423ec2 --- /dev/null +++ b/prizetap/migrations/0075_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2024-08-22 12:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0074_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('prizetap.HaveUnitapPass', 'HaveUnitapPass'), ('prizetap.NotHaveUnitapPass', 'NotHaveUnitapPass'), ('faucet.OptimismDonationConstraint', 'OptimismDonationConstraint'), ('faucet.OptimismClaimingGasConstraint', 'OptimismClaimingGasConstraint')], max_length=255, unique=True), + ), + ] diff --git a/tokenTap/migrations/0061_alter_constraint_name.py b/tokenTap/migrations/0061_alter_constraint_name.py new file mode 100644 index 0000000..4fede02 --- /dev/null +++ b/tokenTap/migrations/0061_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2024-08-22 12:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0060_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('tokenTap.OncePerMonthVerification', 'OncePerMonthVerification'), ('tokenTap.OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('faucet.OptimismHasClaimedGasConstraint', 'OptimismHasClaimedGasConstraint')], max_length=255, unique=True), + ), + ] From e04c36e596afd0ef5e6c5e53a68db35afc5abbcd Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Thu, 22 Aug 2024 17:58:10 +0330 Subject: [PATCH 003/110] Remove chain from GLMStaking --- core/constraints/octant.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/constraints/octant.py b/core/constraints/octant.py index bf37732..fbb5a73 100644 --- a/core/constraints/octant.py +++ b/core/constraints/octant.py @@ -13,7 +13,6 @@ class GLMStakingVerification(ConstraintVerification): app_name = ConstraintApp.OCTANT.value _param_keys = [ - ConstraintParam.CHAIN, ConstraintParam.MINIMUM, ] GLM_CONTRACT_ADDRESS = "0x879133Fd79b7F48CE1c368b0fCA9ea168eaF117c" From d87ddce378eae32a312816b166f47e9dc35ca458 Mon Sep 17 00:00:00 2001 From: pooya <48365551+PooyaFekri@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:01:07 +0330 Subject: [PATCH 004/110] Revert "Remove chain from GLMStaking params" --- core/constraints/__init__.py | 2 +- core/constraints/abstract.py | 2 +- .../constraints/{octant.py => glm_staking.py} | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) rename core/constraints/{octant.py => glm_staking.py} (76%) diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index 06bd36c..7e599bc 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -38,6 +38,7 @@ HasGitcoinPassportProfile, HasMinimumHumanityScore, ) +from core.constraints.glm_staking import GLMStakingVerification from core.constraints.lens import ( BeFollowedByLensUser, DidCollectLensPublication, @@ -48,7 +49,6 @@ IsFollowingLensUser, ) from core.constraints.muon_node import HasMuonNode -from core.constraints.octant import GLMStakingVerification from core.constraints.optimism import DelegateOP, DidDelegateOPToAddress from core.constraints.twitter import ( BeFollowedByTwitterUser, diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index d4c596f..a47bce9 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -15,7 +15,7 @@ class ConstraintApp(Enum): TWITTER = "twitter" MUON = "muon" OPTIMISM = "optimism" - OCTANT = "octant" + GLM_STAKING = "glm_staking" @classmethod def choices(cls): diff --git a/core/constraints/octant.py b/core/constraints/glm_staking.py similarity index 76% rename from core/constraints/octant.py rename to core/constraints/glm_staking.py index bf37732..3aeb02f 100644 --- a/core/constraints/octant.py +++ b/core/constraints/glm_staking.py @@ -1,16 +1,12 @@ +from core.constraints.abstract import ConstraintParam, ConstraintVerification, ConstraintApp +from core.utils import Web3Utils, InvalidAddressException from rest_framework.exceptions import ValidationError - from core.constants import GLM_ABI -from core.constraints.abstract import ( - ConstraintApp, - ConstraintParam, - ConstraintVerification, -) -from core.utils import InvalidAddressException, Web3Utils + class GLMStakingVerification(ConstraintVerification): - app_name = ConstraintApp.OCTANT.value + app_name = ConstraintApp.GLM_STAKING.value _param_keys = [ ConstraintParam.CHAIN, @@ -24,22 +20,26 @@ def __init__(self, user_profile) -> None: def is_observed(self, *args, **kwargs): from core.models import Chain + chain_pk = self.param_values[ConstraintParam.CHAIN.name] + minimum = self.param_values[ConstraintParam.MINIMUM.name] - chain = Chain.objects.get(chain_id="1") + chain = Chain.objects.get(pk=chain_pk) web3_utils = Web3Utils(chain.rpc_url_private, chain.poa) + staked_amount = 0 try: for wallet in self.user_addresses: - staked_amount += self.get_staked_amount(wallet, web3_utils) - + staked_amount += self.get_staked_amount(wallet, web3_utils) + except InvalidAddressException as e: raise ValidationError({"address": str(e)}) - + return staked_amount >= minimum def get_staked_amount(self, user_address: str, web3_utils: Web3Utils) -> int: web3_utils.set_contract(self.GLM_CONTRACT_ADDRESS, GLM_ABI) deposits_function = web3_utils.contract.functions.deposits(user_address) return web3_utils.contract_call(deposits_function) + From 9779eeede0d7bd3ca2b1d44b4199efb4a6851b5e Mon Sep 17 00:00:00 2001 From: Shayan Date: Sat, 24 Aug 2024 03:37:15 +0330 Subject: [PATCH 005/110] gitcoin graph --- core/constraints/gitcoin_passport.py | 38 ++++++++++++---------------- core/thirdpartyapp/gitcoin_graph.py | 21 +++++++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) create mode 100644 core/thirdpartyapp/gitcoin_graph.py diff --git a/core/constraints/gitcoin_passport.py b/core/constraints/gitcoin_passport.py index 49ba054..b7da02f 100644 --- a/core/constraints/gitcoin_passport.py +++ b/core/constraints/gitcoin_passport.py @@ -8,7 +8,7 @@ ConstraintVerification, ) -from ..thirdpartyapp.subgraph import Subgraph +from ..thirdpartyapp.gitcoin_graph import GitcoinGraph class HasGitcoinPassportProfile(ConstraintVerification): @@ -87,34 +87,28 @@ def is_observed(self, *args, **kwargs) -> bool: return False def has_donated(self, min, num_of_projects, round): - subgraph = Subgraph(self._graph_url) + graph = GitcoinGraph() user_wallets = self.user_profile.wallets.values_list( Lower("address"), flat=True ) - query = """ - query getDonationsByDonorAddress($addresses: [String!]!, $round: Int!) { - donations( - filter: { - donorAddress: {in: $addresses} - roundId: { equalTo: $round } - } - ) { - id - amountInUsd - projectId + query = ( + "\n query getDonationsByDonorAddress($address: [String!]!) {\n" + " donations(\n first: 1000\n filter: {\n" + " donorAddress: { in: $address }\n }\n )" + " {\n id\n projectId\n amountInUsd\n }\n }\n" + ) + vars = {"address": list(user_wallets)} + + res = graph.send_post_request( + { + "query": query, + "variables": vars, + "operationName": "getDonationsByDonorAddress", } - } - """ - vars = {"addresses": list(user_wallets), "round": round} - - res = subgraph.send_post_request( - subgraph.path.get("gitcoin"), - query=query, - vars=vars, - other_json_params={"operationName": "getDonationsByDonorAddress"}, ) + print(res) match res: case {"data": {"donations": donations}} if len(donations) > 0: donated_projects = [] diff --git a/core/thirdpartyapp/gitcoin_graph.py b/core/thirdpartyapp/gitcoin_graph.py new file mode 100644 index 0000000..8785a83 --- /dev/null +++ b/core/thirdpartyapp/gitcoin_graph.py @@ -0,0 +1,21 @@ +import json +import logging + +import requests +import zstd + + +class GitcoinGraph: + URL = "https://grants-stack-indexer-v2.gitcoin.co/graphql" + + def send_post_request(self, json_data): + try: + res = requests.post( + self.URL, headers={"Content-Type": "application/json"}, json=json_data + ) + print(res.content.startswith(b"\x28\xb5\x2f\xfd")) + print() + return json.loads(zstd.decompress(res.content)) + except Exception as e: + print(e) + logging.error("Could not connect to gitcoin graph API") From d681848dd070ca82b3fdb7dfc82647a0fa4dbf2c Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Sat, 24 Aug 2024 11:40:51 +0330 Subject: [PATCH 006/110] Fix GLM str and int comparision --- core/constants.py | 18 +++++++----------- core/constraints/octant.py | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/core/constants.py b/core/constants.py index 6566454..71723ca 100644 --- a/core/constants.py +++ b/core/constants.py @@ -184,18 +184,14 @@ ], "name": "Transfer", "type": "event", - } + }, ] -GLM_ABI=[ +GLM_ABI = [ { - "inputs":[ - {"internalType":"address","name":"","type":"address"} - ], - "name":"deposits", - "outputs":[ - {"internalType":"uint256","name":"","type":"uint256"} - ], - "stateMutability":"view", - "type":"function" + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "deposits", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", } ] diff --git a/core/constraints/octant.py b/core/constraints/octant.py index fbb5a73..15b1af7 100644 --- a/core/constraints/octant.py +++ b/core/constraints/octant.py @@ -36,7 +36,7 @@ def is_observed(self, *args, **kwargs): except InvalidAddressException as e: raise ValidationError({"address": str(e)}) - return staked_amount >= minimum + return int(staked_amount) >= int(minimum) def get_staked_amount(self, user_address: str, web3_utils: Web3Utils) -> int: web3_utils.set_contract(self.GLM_CONTRACT_ADDRESS, GLM_ABI) From 64cb503294425580785895fbe3ca7b88467d5038 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Sat, 24 Aug 2024 11:51:15 +0330 Subject: [PATCH 007/110] Fix TestGLMStakingConstraint --- core/tests.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/tests.py b/core/tests.py index a78ec19..300b8bb 100644 --- a/core/tests.py +++ b/core/tests.py @@ -17,6 +17,7 @@ BeAttestedBy, BrightIDAuraVerification, BrightIDMeetVerification, + GLMStakingVerification, HasCommentOnATweet, HasGitcoinPassportProfile, HasMinimumHumanityScore, @@ -26,7 +27,6 @@ HasTokenVerification, HasTwitter, HasVoteOnATweet, - GLMStakingVerification ) test_wallet_key = "f57fecd11c6034fd2665d622e866f05f9b07f35f253ebd5563e3d7e76ae66809" @@ -535,8 +535,6 @@ def test_twitter_liked_tweet_constraint_fail(self): self.assertEqual(constraint.is_observed(), False) -from unittest.mock import patch - class TestGLMStakingConstraint(BaseTestCase): def setUp(self): super().setUp() @@ -562,11 +560,9 @@ def setUp(self): ) def test_glm_staking_constraint_true(self): - constraint = GLMStakingVerification(self.user_profile) constraint.param_values = { - "CHAIN": self.chain.pk, "MINIMUM": self.minimum_staked, } From 6a6a9ec5318bcefef738511fe881e5e76d53411b Mon Sep 17 00:00:00 2001 From: Shayan Shiravani Date: Sat, 24 Aug 2024 14:17:54 +0330 Subject: [PATCH 008/110] bugfix --- core/constraints/gitcoin_passport.py | 23 ++++++++++++++++------- core/thirdpartyapp/gitcoin_graph.py | 6 ++---- core/thirdpartyapp/subgraph.py | 13 ++++--------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/core/constraints/gitcoin_passport.py b/core/constraints/gitcoin_passport.py index b7da02f..39d0c6b 100644 --- a/core/constraints/gitcoin_passport.py +++ b/core/constraints/gitcoin_passport.py @@ -93,13 +93,22 @@ def has_donated(self, min, num_of_projects, round): Lower("address"), flat=True ) - query = ( - "\n query getDonationsByDonorAddress($address: [String!]!) {\n" - " donations(\n first: 1000\n filter: {\n" - " donorAddress: { in: $address }\n }\n )" - " {\n id\n projectId\n amountInUsd\n }\n }\n" - ) - vars = {"address": list(user_wallets)} + query = """ + query getDonationsByDonorAddress($address: [String!]!, $round: String!) { + donations( + first: 1000 + filter: { + donorAddress: { in: $address } + roundId: { equalTo: $round } + } + ) { + id + projectId + amountInUsd + } + } + """ + vars = {"address": list(user_wallets), "round": str(round)} res = graph.send_post_request( { diff --git a/core/thirdpartyapp/gitcoin_graph.py b/core/thirdpartyapp/gitcoin_graph.py index 8785a83..3e88a8d 100644 --- a/core/thirdpartyapp/gitcoin_graph.py +++ b/core/thirdpartyapp/gitcoin_graph.py @@ -2,7 +2,7 @@ import logging import requests -import zstd +import zstandard as zstd class GitcoinGraph: @@ -13,9 +13,7 @@ def send_post_request(self, json_data): res = requests.post( self.URL, headers={"Content-Type": "application/json"}, json=json_data ) - print(res.content.startswith(b"\x28\xb5\x2f\xfd")) - print() - return json.loads(zstd.decompress(res.content)) + return json.loads(zstd.decompress(res.content, 1073741824).decode()) except Exception as e: print(e) logging.error("Could not connect to gitcoin graph API") diff --git a/core/thirdpartyapp/subgraph.py b/core/thirdpartyapp/subgraph.py index fda90df..64ed81c 100644 --- a/core/thirdpartyapp/subgraph.py +++ b/core/thirdpartyapp/subgraph.py @@ -6,28 +6,23 @@ class Subgraph: + requests = RequestHelper(config.SUBGRAPH_BASE_URL) path = { "unitap_pass": "query/73675/unitap-pass-eth/version/latest", "arb_bridge_mainnet": "query/21879/unitap-arb-bridge-mainnet/version/latest", - "gitcoin": "graphql", } - def __init__(self, base_url=None): - self.requests = ( - RequestHelper(base_url) - if base_url - else RequestHelper(config.SUBGRAPH_BASE_URL) - ) + def __init__(self): self.session = self.requests.get_session() def __del__(self): self.session.close() - def send_post_request(self, path, query, vars, other_json_params=None, **kwargs): + def send_post_request(self, path, query, vars, **kwargs): try: return self.requests.post( path=path, - json={"query": query, "variables": vars, **other_json_params}, + json={"query": query, "variables": vars}, session=self.session, **kwargs, ) From 104a2a70af5809f252733fd390bd56cda5187d30 Mon Sep 17 00:00:00 2001 From: Shayan Shiravani Date: Sat, 24 Aug 2024 14:19:58 +0330 Subject: [PATCH 009/110] cleanup --- core/constraints/gitcoin_passport.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/constraints/gitcoin_passport.py b/core/constraints/gitcoin_passport.py index 39d0c6b..0c45246 100644 --- a/core/constraints/gitcoin_passport.py +++ b/core/constraints/gitcoin_passport.py @@ -117,7 +117,6 @@ def has_donated(self, min, num_of_projects, round): "operationName": "getDonationsByDonorAddress", } ) - print(res) match res: case {"data": {"donations": donations}} if len(donations) > 0: donated_projects = [] From e65b5337105578dcbc8eda9f731188a7d3839ea6 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 13:21:54 +0330 Subject: [PATCH 010/110] added captcha requirement --- Dockerfile | 2 +- brightIDfaucet/settings.py | 2 + core/constraints/__init__.py | 1 + core/constraints/abstract.py | 3 +- core/constraints/captcha.py | 42 +++++++++++++++++++ core/models.py | 2 + core/thirdpartyapp/cloudflare.py | 18 ++++++++ .../migrations/0076_alter_constraint_name.py | 18 ++++++++ prizetap/validators.py | 3 +- prizetap/views.py | 4 +- start_dev.sh | 6 +-- .../migrations/0062_alter_constraint_name.py | 18 ++++++++ tokenTap/validators.py | 5 ++- tokenTap/views.py | 4 +- 14 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 core/constraints/captcha.py create mode 100644 core/thirdpartyapp/cloudflare.py create mode 100644 prizetap/migrations/0076_alter_constraint_name.py create mode 100644 tokenTap/migrations/0062_alter_constraint_name.py diff --git a/Dockerfile b/Dockerfile index 96a0855..035c9e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN pip install -r requirements.txt COPY . . RUN mkdir db RUN mkdir -p static -RUN mkdir media +RUN mkdir media -p RUN chmod +x start_dev.sh EXPOSE 5678 diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 7e1e82f..560f962 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -72,6 +72,8 @@ def str2bool(v): MEMCACHED_PASSWORD = os.environ.get("MEMCACHEDCLOUD_PASSWORD") DEPLOYMENT_ENV = os.environ.get("DEPLOYMENT_ENV") +CLOUDFLARE_TURNSITE_SECRET_KEY = os.environ.get("CLOUDFLARE_TURNSITE_SECRET_KEY") + assert DEPLOYMENT_ENV in ["dev", "main"] diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index edb7290..648d948 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -63,6 +63,7 @@ IsFollowingTwitterBatch, IsFollowinTwitterUser, ) +from core.constraints.captcha import HasVerifiedCloudflareCaptcha def get_constraint(constraint_label: str) -> ConstraintVerification: diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index 5292c94..e0f70f9 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -56,9 +56,10 @@ class ConstraintVerification(ABC): app_name = ConstraintApp.GENERAL.value __response_text = "" - def __init__(self, user_profile) -> None: + def __init__(self, user_profile, context=None) -> None: self.user_profile = user_profile self._param_values = {} + self.context = context def get_info(self, *args, **kwargs): pass diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py new file mode 100644 index 0000000..a8bb59c --- /dev/null +++ b/core/constraints/captcha.py @@ -0,0 +1,42 @@ + + +from django.http import HttpRequest +from core.constraints.abstract import ConstraintApp, ConstraintVerification +from core.thirdpartyapp.cloudflare import CloudflareUtil + + +import logging + + +logger = logging.getLogger(__name__) + + + +class HasVerifiedCloudflareCaptcha(ConstraintVerification): + _param_keys = [] + app_name = ConstraintApp.GENERAL.value + + @staticmethod + def get_client_ip(request: HttpRequest) -> str: + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META['REMOTE_ADDR'] + return ip + + def is_observed(self, *args, **kwargs) -> bool: + cloudflare = CloudflareUtil() + + if self.context is None or self.context.get("request") is None: + return False + + request = self.context["request"] + + token = request.data.get("cf-turnstile-response") or request.query_params.get("cf-turnstile-response") + + + if not token: + return False + + return cloudflare.is_verified(token, self.get_client_ip(request)) diff --git a/core/models.py b/core/models.py index 99b7070..547fbb5 100644 --- a/core/models.py +++ b/core/models.py @@ -6,6 +6,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils.translation import gettext_lazy as _ +from core.constraints.captcha import HasVerifiedCloudflareCaptcha from encrypted_model_fields.fields import EncryptedCharField from rest_framework.exceptions import ValidationError from solders.keypair import Keypair @@ -155,6 +156,7 @@ class Type(models.TextChoices): GLMStakingVerification, IsFollowingTwitterBatch, IsFollowingFarcasterBatch, + HasVerifiedCloudflareCaptcha, ] name = models.CharField( diff --git a/core/thirdpartyapp/cloudflare.py b/core/thirdpartyapp/cloudflare.py new file mode 100644 index 0000000..4f1ba42 --- /dev/null +++ b/core/thirdpartyapp/cloudflare.py @@ -0,0 +1,18 @@ +from django.conf import settings +import requests + + +class CloudflareUtil: + def __init__(self) -> None: + self.api_url = "https://challenges.cloudflare.com/turnstile/v0" + self.secret_key = settings.CLOUDFLARE_TURNSITE_SECRET_KEY + + + def is_verified(self, token: str, ip: str) -> bool: + res = requests.post(self.api_url + "/siteverify", data={ + "secret": self.secret_key, + "response": token, + "remoteip": ip + }) + + return res.ok and res.json()["success"] diff --git a/prizetap/migrations/0076_alter_constraint_name.py b/prizetap/migrations/0076_alter_constraint_name.py new file mode 100644 index 0000000..20206c8 --- /dev/null +++ b/prizetap/migrations/0076_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2024-08-25 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0075_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('prizetap.HaveUnitapPass', 'HaveUnitapPass'), ('prizetap.NotHaveUnitapPass', 'NotHaveUnitapPass'), ('faucet.OptimismDonationConstraint', 'OptimismDonationConstraint'), ('faucet.OptimismClaimingGasConstraint', 'OptimismClaimingGasConstraint')], max_length=255, unique=True), + ), + ] diff --git a/prizetap/validators.py b/prizetap/validators.py index 648ce83..ae0023c 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -15,6 +15,7 @@ def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] self.raffle: Raffle = kwargs["raffle"] self.raffle_data: dict = kwargs.get("raffle_data", dict()) + self.request = kwargs.get("request", None) def can_enroll_in_raffle(self): if not self.raffle.is_claimable: @@ -29,7 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile + self.user_profile, { "request": self.request } ) constraint.response = c.response try: diff --git a/prizetap/views.py b/prizetap/views.py index d28cb86..23db587 100644 --- a/prizetap/views.py +++ b/prizetap/views.py @@ -82,7 +82,7 @@ def post(self, request, pk): ) validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data + user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=request ) validator.is_valid(self.request.data) @@ -192,7 +192,7 @@ def get(self, request, raffle_pk): reversed_constraints = raffle.reversed_constraints_list response_constraints = [] validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data + user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=request ) validated_constraints = validator.check_user_constraints(raise_exception=False) diff --git a/start_dev.sh b/start_dev.sh index 63d53e9..2f9c95f 100755 --- a/start_dev.sh +++ b/start_dev.sh @@ -1,5 +1,5 @@ #!/bin/bash python manage.py collectstatic --noinput -python manage.py migrate -python manage.py runserver 0.0.0.0:5678 & -celery -A brightIDfaucet worker -B \ No newline at end of file +# python manage.py migrate +python manage.py runserver 0.0.0.0:5678 +# celery -A brightIDfaucet worker -B \ No newline at end of file diff --git a/tokenTap/migrations/0062_alter_constraint_name.py b/tokenTap/migrations/0062_alter_constraint_name.py new file mode 100644 index 0000000..06f2b7e --- /dev/null +++ b/tokenTap/migrations/0062_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2024-08-25 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0061_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('tokenTap.OncePerMonthVerification', 'OncePerMonthVerification'), ('tokenTap.OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('faucet.OptimismHasClaimedGasConstraint', 'OptimismHasClaimedGasConstraint')], max_length=255, unique=True), + ), + ] diff --git a/tokenTap/validators.py b/tokenTap/validators.py index c10db4c..129d8b8 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -38,11 +38,12 @@ def is_valid(self, data): class TokenDistributionValidator: def __init__( - self, td: TokenDistribution, user_profile: UserProfile, td_data: dict + self, td: TokenDistribution, user_profile: UserProfile, td_data: dict, *args, **kwargs ) -> None: self.td = td self.td_data = td_data self.user_profile = user_profile + self.request = kwargs.get("request") def check_user_permissions(self, raise_exception=True): try: @@ -54,7 +55,7 @@ def check_user_permissions(self, raise_exception=True): result = dict() for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile + self.user_profile, { "request": self.request } ) constraint.response = c.response try: diff --git a/tokenTap/views.py b/tokenTap/views.py index 0467b64..c4eae75 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -133,7 +133,7 @@ def post(self, request, *args, **kwargs): pass validator = TokenDistributionValidator( - token_distribution, user_profile, td_data + token_distribution, user_profile, td_data, request=request ) validator.is_valid() @@ -184,7 +184,7 @@ def get(self, request, td_id): reversed_constraints = td.reversed_constraints_list response_constraints = [] - validator = TokenDistributionValidator(td, user_profile, td_data) + validator = TokenDistributionValidator(td, user_profile, td_data, request=request) validated_constraints = validator.check_user_permissions(raise_exception=False) for c_pk, data in validated_constraints.items(): response_constraints.append( From 9c76602835c6dfb03a8b851db82b561cc55429b9 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 15:41:32 +0330 Subject: [PATCH 011/110] rollback previous comments for running dev --- start_dev.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/start_dev.sh b/start_dev.sh index 2f9c95f..8d8604f 100755 --- a/start_dev.sh +++ b/start_dev.sh @@ -1,5 +1,5 @@ #!/bin/bash python manage.py collectstatic --noinput -# python manage.py migrate +python manage.py migrate python manage.py runserver 0.0.0.0:5678 -# celery -A brightIDfaucet worker -B \ No newline at end of file +celery -A brightIDfaucet worker -B \ No newline at end of file From 550dfe998e4aa9f7c03c4c5553113d6d89249de6 Mon Sep 17 00:00:00 2001 From: Ali Maktabi <67747298+alimaktabi@users.noreply.github.com> Date: Sun, 25 Aug 2024 15:47:11 +0330 Subject: [PATCH 012/110] Update core/constraints/captcha.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- core/constraints/captcha.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index a8bb59c..4bd8183 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -18,12 +18,11 @@ class HasVerifiedCloudflareCaptcha(ConstraintVerification): @staticmethod def get_client_ip(request: HttpRequest) -> str: - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META['REMOTE_ADDR'] - return ip + return ( + x_forwarded_for.split(',')[0] + if (x_forwarded_for := request.META.get('HTTP_X_FORWARDED_FOR')) + else request.META['REMOTE_ADDR'] + ) def is_observed(self, *args, **kwargs) -> bool: cloudflare = CloudflareUtil() From 8b5acf4e25ebd10e49748686a46f33ed95a2c596 Mon Sep 17 00:00:00 2001 From: Ali Maktabi <67747298+alimaktabi@users.noreply.github.com> Date: Sun, 25 Aug 2024 15:47:30 +0330 Subject: [PATCH 013/110] Update core/thirdpartyapp/cloudflare.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- core/thirdpartyapp/cloudflare.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/thirdpartyapp/cloudflare.py b/core/thirdpartyapp/cloudflare.py index 4f1ba42..85a0b33 100644 --- a/core/thirdpartyapp/cloudflare.py +++ b/core/thirdpartyapp/cloudflare.py @@ -9,10 +9,13 @@ def __init__(self) -> None: def is_verified(self, token: str, ip: str) -> bool: - res = requests.post(self.api_url + "/siteverify", data={ - "secret": self.secret_key, - "response": token, - "remoteip": ip - }) + res = requests.post( + f"{self.api_url}/siteverify", + data={ + "secret": self.secret_key, + "response": token, + "remoteip": ip + }, + ) return res.ok and res.json()["success"] From 13c8530f9e14ffa63c4dd3f2285aa6b19b6d1335 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 15:48:37 +0330 Subject: [PATCH 014/110] refactored namings and expressions --- core/constraints/captcha.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index a8bb59c..6374fd7 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -33,10 +33,6 @@ def is_observed(self, *args, **kwargs) -> bool: request = self.context["request"] - token = request.data.get("cf-turnstile-response") or request.query_params.get("cf-turnstile-response") + turnstile_token = request.data.get("cf-turnstile-response") or request.query_params.get("cf-turnstile-response") - - if not token: - return False - - return cloudflare.is_verified(token, self.get_client_ip(request)) + return turnstile_token is not None and cloudflare.is_verified(turnstile_token, self.get_client_ip(request)) From 2d542698a47328e4eee7de11d8f0f1ba71862cf2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 15:50:10 +0330 Subject: [PATCH 015/110] added error handling on cloudflare api --- core/thirdpartyapp/cloudflare.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/core/thirdpartyapp/cloudflare.py b/core/thirdpartyapp/cloudflare.py index 85a0b33..d1cefbd 100644 --- a/core/thirdpartyapp/cloudflare.py +++ b/core/thirdpartyapp/cloudflare.py @@ -1,7 +1,10 @@ +import logging from django.conf import settings import requests +logger = logging.getLogger(__name__) + class CloudflareUtil: def __init__(self) -> None: self.api_url = "https://challenges.cloudflare.com/turnstile/v0" @@ -9,13 +12,18 @@ def __init__(self) -> None: def is_verified(self, token: str, ip: str) -> bool: - res = requests.post( - f"{self.api_url}/siteverify", - data={ - "secret": self.secret_key, - "response": token, - "remoteip": ip - }, - ) + try: + res = requests.post( + f"{self.api_url}/siteverify", + data={ + "secret": self.secret_key, + "response": token, + "remoteip": ip + }, + ) + + return res.ok and res.json()["success"] + except Exception as e: + logger.info(f"Error occurred during cloudflare verification {str(e)}") - return res.ok and res.json()["success"] + return False \ No newline at end of file From 2ce9491dbd97f44c5448bed803af50525d18ef18 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 15:52:54 +0330 Subject: [PATCH 016/110] refactored get_client ip behavior --- core/constraints/captcha.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index 1d8b624..d212b30 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -17,12 +17,14 @@ class HasVerifiedCloudflareCaptcha(ConstraintVerification): app_name = ConstraintApp.GENERAL.value @staticmethod - def get_client_ip(request: HttpRequest) -> str: - return ( - x_forwarded_for.split(',')[0] - if (x_forwarded_for := request.META.get('HTTP_X_FORWARDED_FOR')) - else request.META['REMOTE_ADDR'] - ) + def get_client_ip(x_forwarded_for): + if x_forwarded_for: + ip_list = [ip.strip() for ip in x_forwarded_for.split(',')] + for ip in ip_list: + if ip and not ip.startswith(('10.', '172.16.', '192.168.')): + return ip + return None + def is_observed(self, *args, **kwargs) -> bool: cloudflare = CloudflareUtil() @@ -34,4 +36,10 @@ def is_observed(self, *args, **kwargs) -> bool: turnstile_token = request.data.get("cf-turnstile-response") or request.query_params.get("cf-turnstile-response") - return turnstile_token is not None and cloudflare.is_verified(turnstile_token, self.get_client_ip(request)) + return ( + turnstile_token is not None and + cloudflare.is_verified( + turnstile_token, + self.get_client_ip(request.META.get('HTTP_X_FORWARDED_FOR') or request.META['REMOTE_ADDR']) # type: ignore + ) + ) From 08afef78d857eb7530b985330d8bbc394ac76e2e Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 16:00:28 +0330 Subject: [PATCH 017/110] refactored code to remove passing request object to constraint classes --- core/constraints/captcha.py | 17 +++++------------ core/utils.py | 21 +++++++++++++++++++++ prizetap/validators.py | 3 ++- tokenTap/views.py | 3 ++- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index d212b30..dc152f4 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -7,6 +7,8 @@ import logging +from core.utils import RequestContextExtractor + logger = logging.getLogger(__name__) @@ -16,15 +18,6 @@ class HasVerifiedCloudflareCaptcha(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.GENERAL.value - @staticmethod - def get_client_ip(x_forwarded_for): - if x_forwarded_for: - ip_list = [ip.strip() for ip in x_forwarded_for.split(',')] - for ip in ip_list: - if ip and not ip.startswith(('10.', '172.16.', '192.168.')): - return ip - return None - def is_observed(self, *args, **kwargs) -> bool: cloudflare = CloudflareUtil() @@ -32,14 +25,14 @@ def is_observed(self, *args, **kwargs) -> bool: if self.context is None or self.context.get("request") is None: return False - request = self.context["request"] + request: RequestContextExtractor = self.context["request"] - turnstile_token = request.data.get("cf-turnstile-response") or request.query_params.get("cf-turnstile-response") + turnstile_token = request.data.get("cf-turnstile-response") return ( turnstile_token is not None and cloudflare.is_verified( turnstile_token, - self.get_client_ip(request.META.get('HTTP_X_FORWARDED_FOR') or request.META['REMOTE_ADDR']) # type: ignore + self.get_client_ip(request.ip) # type: ignore ) ) diff --git a/core/utils.py b/core/utils.py index 268d707..0375c00 100644 --- a/core/utils.py +++ b/core/utils.py @@ -5,6 +5,7 @@ import uuid from contextlib import contextmanager +from django.http import HttpRequest import pytz import web3.exceptions from django.core.cache import cache @@ -377,3 +378,23 @@ def save(self, file: UploadedFile): str(self.upload_to or "") + file_name + file_extension, file ) return MEDIA_ROOT + "/" + path + + + + +class RequestContextExtractor: + def __init__(self, request) -> None: + self.headers = request.headers + self.ip = RequestContextExtractor.get_client_ip(request.META.get('HTTP_X_FORWARDED_FOR') or request.META['REMOTE_ADDR']) + self.data = {**request.query_params, **request.data} + + @staticmethod + def get_client_ip(x_forwarded_for): + if x_forwarded_for: + ip_list = [ip.strip() for ip in x_forwarded_for.split(',')] + for ip in ip_list: + if ip and not ip.startswith(('10.', '172.16.', '192.168.')): + return ip + return None + + \ No newline at end of file diff --git a/prizetap/validators.py b/prizetap/validators.py index ae0023c..e3026e8 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -6,6 +6,7 @@ from authentication.models import UserProfile from core.constraints import ConstraintVerification, get_constraint +from core.utils import RequestContextExtractor from .models import Raffle, RaffleEntry @@ -30,7 +31,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, { "request": self.request } + self.user_profile, { "request": RequestContextExtractor(self.request) } ) constraint.response = c.response try: diff --git a/tokenTap/views.py b/tokenTap/views.py index c4eae75..e057907 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -2,6 +2,7 @@ from django.db import transaction from django.http import Http404 from django.shortcuts import get_object_or_404 +from core.utils import RequestContextExtractor from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework.exceptions import PermissionDenied @@ -133,7 +134,7 @@ def post(self, request, *args, **kwargs): pass validator = TokenDistributionValidator( - token_distribution, user_profile, td_data, request=request + token_distribution, user_profile, td_data, request=RequestContextExtractor(request) ) validator.is_valid() From 04eea3a038dbddd7d667a377b104a01ae00e9dfe Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 16:09:43 +0330 Subject: [PATCH 018/110] fixed missing request extractor --- tokenTap/validators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 129d8b8..da07375 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -7,6 +7,7 @@ from authentication.models import UserProfile from core.constraints import ConstraintVerification, get_constraint +from core.utils import RequestContextExtractor from .helpers import has_credit_left from .models import ClaimReceipt, TokenDistribution @@ -55,7 +56,7 @@ def check_user_permissions(self, raise_exception=True): result = dict() for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, { "request": self.request } + self.user_profile, { "request": RequestContextExtractor(self.request) } ) constraint.response = c.response try: From bc12366cc9bf3e576f029f4a370b87695bd02b02 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 16:30:30 +0330 Subject: [PATCH 019/110] fixed request context extractor usages --- prizetap/validators.py | 3 +-- prizetap/views.py | 5 +++-- tokenTap/validators.py | 3 +-- tokenTap/views.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/prizetap/validators.py b/prizetap/validators.py index e3026e8..ae0023c 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -6,7 +6,6 @@ from authentication.models import UserProfile from core.constraints import ConstraintVerification, get_constraint -from core.utils import RequestContextExtractor from .models import Raffle, RaffleEntry @@ -31,7 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, { "request": RequestContextExtractor(self.request) } + self.user_profile, { "request": self.request } ) constraint.response = c.response try: diff --git a/prizetap/views.py b/prizetap/views.py index 23db587..517bd49 100644 --- a/prizetap/views.py +++ b/prizetap/views.py @@ -3,6 +3,7 @@ from django.db.models import Case, F, When from django.shortcuts import get_object_or_404 from django.utils import timezone +from core.utils import RequestContextExtractor from drf_yasg.utils import swagger_auto_schema from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveAPIView from rest_framework.parsers import FormParser, MultiPartParser @@ -82,7 +83,7 @@ def post(self, request, pk): ) validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=request + user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=RequestContextExtractor(request) ) validator.is_valid(self.request.data) @@ -192,7 +193,7 @@ def get(self, request, raffle_pk): reversed_constraints = raffle.reversed_constraints_list response_constraints = [] validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=request + user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=RequestContextExtractor(request) ) validated_constraints = validator.check_user_constraints(raise_exception=False) diff --git a/tokenTap/validators.py b/tokenTap/validators.py index da07375..129d8b8 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -7,7 +7,6 @@ from authentication.models import UserProfile from core.constraints import ConstraintVerification, get_constraint -from core.utils import RequestContextExtractor from .helpers import has_credit_left from .models import ClaimReceipt, TokenDistribution @@ -56,7 +55,7 @@ def check_user_permissions(self, raise_exception=True): result = dict() for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, { "request": RequestContextExtractor(self.request) } + self.user_profile, { "request": self.request } ) constraint.response = c.response try: diff --git a/tokenTap/views.py b/tokenTap/views.py index e057907..eb1b80e 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -185,7 +185,7 @@ def get(self, request, td_id): reversed_constraints = td.reversed_constraints_list response_constraints = [] - validator = TokenDistributionValidator(td, user_profile, td_data, request=request) + validator = TokenDistributionValidator(td, user_profile, td_data, request=RequestContextExtractor(request)) validated_constraints = validator.check_user_permissions(raise_exception=False) for c_pk, data in validated_constraints.items(): response_constraints.append( From 82bbc2579441bc04d394ee8947f9d140a028bdb6 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 18:07:37 +0330 Subject: [PATCH 020/110] added cloudflare images field --- brightIDfaucet/settings.py | 5 ++ core/fields.py | 103 +++++++++++++++++++++++++++ core/services.py | 101 ++++++++++++++++++++++++++ core/storages.py | 141 +++++++++++++++++++++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 core/fields.py create mode 100644 core/services.py create mode 100644 core/storages.py diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 7e1e82f..86d6db1 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -72,6 +72,11 @@ def str2bool(v): MEMCACHED_PASSWORD = os.environ.get("MEMCACHEDCLOUD_PASSWORD") DEPLOYMENT_ENV = os.environ.get("DEPLOYMENT_ENV") +CLOUDFLARE_IMAGES_ACCOUNT_ID = os.environ.get("CLOUDFLARE_ACCOUNT_ID") +CLOUDFLARE_IMAGES_API_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN") +CLOUDFLARE_IMAGES_ACCOUNT_HASH = os.environ.get("CLOUDFLARE_ACCOUNT_HASH") + + assert DEPLOYMENT_ENV in ["dev", "main"] diff --git a/core/fields.py b/core/fields.py new file mode 100644 index 0000000..4262ae1 --- /dev/null +++ b/core/fields.py @@ -0,0 +1,103 @@ +""" +Contains all the fields related classes to support custom features +such as variants in Cloudflare Images +""" + +from django.db.models.base import Model +from django.db.models.fields.files import ( + FileField, + ImageFieldFile, + ImageField, + ImageFileDescriptor, +) +from .storages import CloudflareImagesStorage +from django.db import models + + +class CloudflareImagesFileDescriptor(ImageFileDescriptor): + """ + Inherits ImageField's descriptor class + """ + + pass + + +class CloudflareImagesFieldFile(ImageFieldFile): + """ + Inherits ImageField's attr class + """ + storage: CloudflareImagesStorage + + def __init__(self, instance: Model, field: FileField, name: str | None) -> None: + super().__init__(instance, field, name) + + self.storage = CloudflareImagesStorage() + + @property + def url(self): + """ + Overriding the default url method to pass our variant + """ + return self.storage.url_with_variant(self.name, variant=self.field.variant) # type: ignore + + +class CloudflareImagesField(ImageField): + """ + Custom field based on ImageField allowing us to pass a variant + """ + + attr_class = CloudflareImagesFieldFile + descriptor_class = CloudflareImagesFileDescriptor + description = "Image" + storage = CloudflareImagesStorage + + def __init__( + self, + verbose_name=None, + name=None, + width_field=None, + height_field=None, + variant=None, + **kwargs, + ): + """ + Calling ImageFieldFile constructor and setting our variant + """ + self.variant = variant or "public" + super().__init__(verbose_name, name, width_field, height_field, **kwargs) + + def deconstruct(self): + """ + Returns the deconstructed version of our field. + Same as ImageField with variant on top + """ + name, path, args, kwargs = super().deconstruct() + kwargs["variant"] = self.variant + return name, path, args, kwargs + + + + + +class BigNumField(models.Field): + empty_strings_allowed = False + + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 200 # or some other number + super().__init__(*args, **kwargs) + + def db_type(self, connection): + return "numeric" + + def get_internal_type(self): + return "BigNumField" + + def to_python(self, value): + if isinstance(value, str): + return int(value) + + return value + + def get_prep_value(self, value): + return str(value) + diff --git a/core/services.py b/core/services.py new file mode 100644 index 0000000..42338a2 --- /dev/null +++ b/core/services.py @@ -0,0 +1,101 @@ +""" +Contains the Cloudflare Image service which handles the API exchanges +""" +from django.conf import settings + +import requests + + +class ApiException(Exception): + """ + Exception raised by Cloudflare Images API + """ + + pass + + +class CloudflareImagesService: + """ + API client for Cloudflare Images + """ + + def __init__(self): + """ + Loads the configuration + """ + self.account_id = settings.CLOUDFLARE_IMAGES_ACCOUNT_ID + self.api_token = settings.CLOUDFLARE_IMAGES_API_TOKEN + self.account_hash = settings.CLOUDFLARE_IMAGES_ACCOUNT_HASH + self.api_timeout = 60 + self.domain = None + + def upload(self, file): + """ + Uploads a file and return its name, otherwise raise an exception + """ + url = "https://api.cloudflare.com/client/v4/accounts/{}/images/v1".format( + self.account_id + ) + + headers = {"Authorization": "Bearer {}".format(self.api_token)} + + files = {"file": file} + + response = requests.post( + url, headers=headers, timeout=self.api_timeout, files=files + ) + + status_code = response.status_code + if status_code != 200: + raise ApiException(response.content) + + response_body = response.json() + return response_body.get("result").get("id") + + def get_url(self, name, variant): + """ + Returns the public URL for the given image ID + """ + + if self.domain: + return "https://{}/cdn-cgi/imagedelivery/{}/{}/{}".format( + self.domain, self.account_hash, name, variant + ) + + return "https://imagedelivery.net/{}/{}/{}".format( + self.account_hash, name, variant + ) + + def open(self, name, variant=None): + """ + Retrieves a file and return its content, otherwise raise an exception + """ + + url = self.get_url(name, variant or "public") + + response = requests.get(url, timeout=self.api_timeout) + + status_code = response.status_code + if status_code != 200: + raise ApiException(response.content) + + return response.content + + def delete(self, name): + """ + Deletes a file if it exists, otherwise raise an exception + """ + + url = "https://api.cloudflare.com/client/v4/accounts/{}/images/v1/{}".format( + self.account_id, name + ) + + headers = {"Authorization": "Bearer {}".format(self.api_token)} + + response = requests.delete( + url, timeout=self.api_timeout, headers=headers + ) + + status_code = response.status_code + if status_code != 200: + raise ApiException(str(response.text)) \ No newline at end of file diff --git a/core/storages.py b/core/storages.py new file mode 100644 index 0000000..2438d4b --- /dev/null +++ b/core/storages.py @@ -0,0 +1,141 @@ +""" +Contains the Cloudflare Image storage which is supposed to replace Django's +default Storage (see README.md) +Django's default storage class: https://github.com/django/django/blob/main/django/core/files/storage.py +""" + +from django.core.files.base import File +from django.core.files.storage import Storage +from django.utils.deconstruct import deconstructible +from .services import CloudflareImagesService + + +@deconstructible +class CloudflareImagesStorage(Storage): + """ + Django storage for Cloudflare Images + """ + + def __init__(self): + """ + Setups the storage + """ + super().__init__() + + self.service = CloudflareImagesService() + + def _open(self, name, mode="rb"): + """ + Returns the image as a File + The parameter "mode" has been kept to respect the original signature + (and it fails without it) but it wont have any impact + Has to be implemented. + """ + content = self.service.open(name) + # print(content, "\n\n\n\n\n\n\n\n\n\n") + # print("FILASDASDASDD", File(content, name=name)) + return File(content, name=name) + + def _save(self, name, content): + """ + Tries to upload the file and return its name + Has to be implemented. + """ + new_name = self.generate_filename(name) + content.name = new_name + return self.service.upload(content) + + def get_valid_name(self, name): + """ + Returns a valid name for the file. + Has to be implemented. + """ + return name + + def get_available_name(self, name, max_length=None): + """ + Returns the available name for the file. + Has to be implemented. + """ + return self.generate_filename(name) + + def generate_filename(self, filename): + """ + Returns the name of the file. + Has to be implemented. + """ + return filename + + def delete(self, name): + """ + Tries to delete the specified file from the storage system. + Has to be implemented. + """ + self.service.delete(name) + + def exists(self, name): + """ + Return True if a file referenced by the given name already exists in the + storage system, or False if the name is available for a new file. + """ + raise NotImplementedError( + "subclasses of Storage must provide an exists() method" + ) + + def listdir(self, path): + """ + List the contents of the specified path. Return a 2-tuple of lists: + the first item being directories, the second item being files. + """ + raise NotImplementedError( + "subclasses of Storage must provide a listdir() method" + ) + + def size(self, name): + """ + Return the total size, in bytes, of the file specified by name. + """ + content = self.service.open(name) + return len(content) + + def url(self, name): + """ + Return an absolute URL where the file's contents can be accessed + directly by a web browser. + Has to be implemented. + """ + return self.url_with_variant(name, 'public') + + def url_with_variant(self, name, variant): + """ + Custom methods which allow to pass a variant and respect the original signature of `url` + """ + print("URL: ") + return self.service.get_url(name, variant) + + def get_accessed_time(self, name): + """ + Return the last accessed time (as a datetime) of the file specified by + name. The datetime will be timezone-aware if USE_TZ=True. + """ + raise NotImplementedError( + "subclasses of Storage must provide a get_accessed_time() method" + ) + + def get_created_time(self, name): + """ + Return the creation time (as a datetime) of the file specified by name. + The datetime will be timezone-aware if USE_TZ=True. + """ + raise NotImplementedError( + "subclasses of Storage must provide a get_created_time() method" + ) + + def get_modified_time(self, name): + """ + Return the last modified time (as a datetime) of the file specified by + name. The datetime will be timezone-aware if USE_TZ=True. + """ + raise NotImplementedError( + "subclasses of Storage must provide a get_modified_time() method" + ) \ No newline at end of file From 2d46d0cb24cf43f6c3353c042e8d4c4620551e16 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 18:09:02 +0330 Subject: [PATCH 021/110] update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0b10be9..6f6be00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,3 +32,4 @@ celery~=5.3.4 django-safedelete~=1.3.3 tweepy~=4.14.0 ratelimit~=2.2.1 +pillow==10.4.0 From 6ce04cf54c4ebce34daef2164106f53ef5d5b8db Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 18:13:28 +0330 Subject: [PATCH 022/110] removed extra prints --- core/storages.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/storages.py b/core/storages.py index 2438d4b..658ed13 100644 --- a/core/storages.py +++ b/core/storages.py @@ -32,8 +32,6 @@ def _open(self, name, mode="rb"): Has to be implemented. """ content = self.service.open(name) - # print(content, "\n\n\n\n\n\n\n\n\n\n") - # print("FILASDASDASDD", File(content, name=name)) return File(content, name=name) def _save(self, name, content): @@ -110,7 +108,6 @@ def url_with_variant(self, name, variant): """ Custom methods which allow to pass a variant and respect the original signature of `url` """ - print("URL: ") return self.service.get_url(name, variant) def get_accessed_time(self, name): From c23e74244479abf39196f9ff1e9190d2502b9614 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 25 Aug 2024 18:15:25 +0330 Subject: [PATCH 023/110] refactored string injection method --- core/services.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/services.py b/core/services.py index 42338a2..54af4bb 100644 --- a/core/services.py +++ b/core/services.py @@ -58,13 +58,9 @@ def get_url(self, name, variant): """ if self.domain: - return "https://{}/cdn-cgi/imagedelivery/{}/{}/{}".format( - self.domain, self.account_hash, name, variant - ) + return f"https://{self.domain}/cdn-cgi/imagedelivery/{self.account_hash}/{name}/{variant}" - return "https://imagedelivery.net/{}/{}/{}".format( - self.account_hash, name, variant - ) + return f"https://imagedelivery.net/{self.account_hash}/{name}/{variant}" def open(self, name, variant=None): """ From ada5b191e05b1a39b05d61b9193afec3eb087d4c Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 26 Aug 2024 12:07:15 +0330 Subject: [PATCH 024/110] renamed request variable --- brightIDfaucet/settings.py | 4 ++-- core/constraints/captcha.py | 22 +++++++--------------- core/thirdpartyapp/cloudflare.py | 31 +++++++++++++------------------ prizetap/validators.py | 4 ++-- prizetap/views.py | 10 ++++++++-- tokenTap/validators.py | 11 ++++++++--- 6 files changed, 40 insertions(+), 42 deletions(-) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 560f962..5a18aee 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -72,7 +72,7 @@ def str2bool(v): MEMCACHED_PASSWORD = os.environ.get("MEMCACHEDCLOUD_PASSWORD") DEPLOYMENT_ENV = os.environ.get("DEPLOYMENT_ENV") -CLOUDFLARE_TURNSITE_SECRET_KEY = os.environ.get("CLOUDFLARE_TURNSITE_SECRET_KEY") +CLOUDFLARE_TURNSTILE_SECRET_KEY = os.environ.get("CLOUDFLARE_TURNSTILE_SECRET_KEY") assert DEPLOYMENT_ENV in ["dev", "main"] @@ -261,4 +261,4 @@ def before_send(event, hint): "djangorestframework_camel_case.parser.CamelCaseJSONParser", ), } -CELERY_BROKER_URL = REDIS_URL \ No newline at end of file +CELERY_BROKER_URL = REDIS_URL diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index dc152f4..71ce9b5 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -1,6 +1,3 @@ - - -from django.http import HttpRequest from core.constraints.abstract import ConstraintApp, ConstraintVerification from core.thirdpartyapp.cloudflare import CloudflareUtil @@ -13,26 +10,21 @@ logger = logging.getLogger(__name__) - class HasVerifiedCloudflareCaptcha(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.GENERAL.value - def is_observed(self, *args, **kwargs) -> bool: - cloudflare = CloudflareUtil() - if self.context is None or self.context.get("request") is None: + if self.context is None or self.context.get("request_context") is None: return False - - request: RequestContextExtractor = self.context["request"] + + cloudflare = CloudflareUtil() + + request: RequestContextExtractor = self.context["request_context"] turnstile_token = request.data.get("cf-turnstile-response") - return ( - turnstile_token is not None and - cloudflare.is_verified( - turnstile_token, - self.get_client_ip(request.ip) # type: ignore - ) + return turnstile_token is not None and cloudflare.is_verified( + turnstile_token, request.ip ) diff --git a/core/thirdpartyapp/cloudflare.py b/core/thirdpartyapp/cloudflare.py index d1cefbd..4687928 100644 --- a/core/thirdpartyapp/cloudflare.py +++ b/core/thirdpartyapp/cloudflare.py @@ -5,25 +5,20 @@ logger = logging.getLogger(__name__) -class CloudflareUtil: - def __init__(self) -> None: - self.api_url = "https://challenges.cloudflare.com/turnstile/v0" - self.secret_key = settings.CLOUDFLARE_TURNSITE_SECRET_KEY +class CloudflareUtil: + secret_key = settings.CLOUDFLARE_TURNSTILE_SECRET_KEY + api_url = "https://challenges.cloudflare.com/turnstile/v0" - def is_verified(self, token: str, ip: str) -> bool: - try: - res = requests.post( - f"{self.api_url}/siteverify", - data={ - "secret": self.secret_key, - "response": token, - "remoteip": ip - }, - ) + def is_verified(self, token: str, ip: str) -> bool: + try: + res = requests.post( + f"{self.api_url}/siteverify", + data={"secret": self.secret_key, "response": token, "remoteip": ip}, + ) - return res.ok and res.json()["success"] - except Exception as e: - logger.info(f"Error occurred during cloudflare verification {str(e)}") + return res.ok and res.json()["success"] + except Exception as e: + logger.info(f"Error occurred during cloudflare verification {str(e)}") - return False \ No newline at end of file + return False diff --git a/prizetap/validators.py b/prizetap/validators.py index ae0023c..f83ebad 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -15,7 +15,7 @@ def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] self.raffle: Raffle = kwargs["raffle"] self.raffle_data: dict = kwargs.get("raffle_data", dict()) - self.request = kwargs.get("request", None) + self.request_context = kwargs.get("request_context", None) def can_enroll_in_raffle(self): if not self.raffle.is_claimable: @@ -30,7 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, { "request": self.request } + self.user_profile, {"request_context": self.request_context} ) constraint.response = c.response try: diff --git a/prizetap/views.py b/prizetap/views.py index 517bd49..2c993ea 100644 --- a/prizetap/views.py +++ b/prizetap/views.py @@ -83,7 +83,10 @@ def post(self, request, pk): ) validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=RequestContextExtractor(request) + user_profile=user_profile, + raffle=raffle, + raffle_data=raffle_data, + request_context=RequestContextExtractor(request), ) validator.is_valid(self.request.data) @@ -193,7 +196,10 @@ def get(self, request, raffle_pk): reversed_constraints = raffle.reversed_constraints_list response_constraints = [] validator = RaffleEnrollmentValidator( - user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, request=RequestContextExtractor(request) + user_profile=user_profile, + raffle=raffle, + raffle_data=raffle_data, + request_context=RequestContextExtractor(request), ) validated_constraints = validator.check_user_constraints(raise_exception=False) diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 129d8b8..d92a234 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -38,12 +38,17 @@ def is_valid(self, data): class TokenDistributionValidator: def __init__( - self, td: TokenDistribution, user_profile: UserProfile, td_data: dict, *args, **kwargs + self, + td: TokenDistribution, + user_profile: UserProfile, + td_data: dict, + *args, + **kwargs, ) -> None: self.td = td self.td_data = td_data self.user_profile = user_profile - self.request = kwargs.get("request") + self.request_context = kwargs.get("request_context") def check_user_permissions(self, raise_exception=True): try: @@ -55,7 +60,7 @@ def check_user_permissions(self, raise_exception=True): result = dict() for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, { "request": self.request } + self.user_profile, {"request_context": self.request_context} ) constraint.response = c.response try: From ee944e108e324db96ff6de345ab7816d787e258b Mon Sep 17 00:00:00 2001 From: Ali Maktabi <67747298+alimaktabi@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:36:52 +0330 Subject: [PATCH 025/110] Update tokenTap/validators.py Co-authored-by: pooya <48365551+PooyaFekri@users.noreply.github.com> --- tokenTap/validators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tokenTap/validators.py b/tokenTap/validators.py index d92a234..1b555de 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -60,7 +60,8 @@ def check_user_permissions(self, raise_exception=True): result = dict() for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, {"request_context": self.request_context} + self.user_profile, + context={"request_context": self.request_context} ) constraint.response = c.response try: From 22fb79239b76e51e8c0ab28fcb89bfc1bfa9b541 Mon Sep 17 00:00:00 2001 From: Ali Maktabi <67747298+alimaktabi@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:37:11 +0330 Subject: [PATCH 026/110] Update prizetap/validators.py Co-authored-by: pooya <48365551+PooyaFekri@users.noreply.github.com> --- prizetap/validators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prizetap/validators.py b/prizetap/validators.py index f83ebad..2747cf6 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -30,7 +30,8 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, {"request_context": self.request_context} + self.user_profile, + context={"request_context": self.request_context} ) constraint.response = c.response try: From 8e6ae5c153d7170213ae6e9f054259febf4a3d1b Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 26 Aug 2024 12:44:48 +0330 Subject: [PATCH 027/110] resolved comments on PR --- core/constraints/captcha.py | 4 +++- prizetap/validators.py | 5 ++--- prizetap/views.py | 5 ++--- tokenTap/views.py | 10 +++++++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index 71ce9b5..d775109 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -21,7 +21,9 @@ def is_observed(self, *args, **kwargs) -> bool: cloudflare = CloudflareUtil() - request: RequestContextExtractor = self.context["request_context"] + request: RequestContextExtractor = RequestContextExtractor( + self.context["request_context"] + ) turnstile_token = request.data.get("cf-turnstile-response") diff --git a/prizetap/validators.py b/prizetap/validators.py index 2747cf6..7a4015f 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -15,7 +15,7 @@ def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] self.raffle: Raffle = kwargs["raffle"] self.raffle_data: dict = kwargs.get("raffle_data", dict()) - self.request_context = kwargs.get("request_context", None) + self.request_context = kwargs.get("request_context") def can_enroll_in_raffle(self): if not self.raffle.is_claimable: @@ -30,8 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, - context={"request_context": self.request_context} + self.user_profile, context={"request_context": self.request_context} ) constraint.response = c.response try: diff --git a/prizetap/views.py b/prizetap/views.py index 2c993ea..18c354e 100644 --- a/prizetap/views.py +++ b/prizetap/views.py @@ -3,7 +3,6 @@ from django.db.models import Case, F, When from django.shortcuts import get_object_or_404 from django.utils import timezone -from core.utils import RequestContextExtractor from drf_yasg.utils import swagger_auto_schema from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveAPIView from rest_framework.parsers import FormParser, MultiPartParser @@ -86,7 +85,7 @@ def post(self, request, pk): user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, - request_context=RequestContextExtractor(request), + request_context=request, ) validator.is_valid(self.request.data) @@ -199,7 +198,7 @@ def get(self, request, raffle_pk): user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, - request_context=RequestContextExtractor(request), + request_context=request, ) validated_constraints = validator.check_user_constraints(raise_exception=False) diff --git a/tokenTap/views.py b/tokenTap/views.py index eb1b80e..a979cfd 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -2,7 +2,6 @@ from django.db import transaction from django.http import Http404 from django.shortcuts import get_object_or_404 -from core.utils import RequestContextExtractor from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework.exceptions import PermissionDenied @@ -134,7 +133,10 @@ def post(self, request, *args, **kwargs): pass validator = TokenDistributionValidator( - token_distribution, user_profile, td_data, request=RequestContextExtractor(request) + token_distribution, + user_profile, + td_data, + request_context=request, ) validator.is_valid() @@ -185,7 +187,9 @@ def get(self, request, td_id): reversed_constraints = td.reversed_constraints_list response_constraints = [] - validator = TokenDistributionValidator(td, user_profile, td_data, request=RequestContextExtractor(request)) + validator = TokenDistributionValidator( + td, user_profile, td_data, request_context=request + ) validated_constraints = validator.check_user_permissions(raise_exception=False) for c_pk, data in validated_constraints.items(): response_constraints.append( From 0775b470b83d5e0738cf64620f8c5af47f997d9b Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 26 Aug 2024 12:53:56 +0330 Subject: [PATCH 028/110] added migration for creating the requirements --- .../migrations/0076_alter_constraint_name.py | 90 ++++++++++++++++++- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/prizetap/migrations/0076_alter_constraint_name.py b/prizetap/migrations/0076_alter_constraint_name.py index 20206c8..8eeae55 100644 --- a/prizetap/migrations/0076_alter_constraint_name.py +++ b/prizetap/migrations/0076_alter_constraint_name.py @@ -3,16 +3,98 @@ from django.db import migrations, models +def create_prizetap_constraint(apps, schema_editor): + Constraint = apps.get_model("prizetap", "Constraint") + + Constraint.objects.create( + name="core.HasVerifiedCloudflareCaptcha", + description="HasVerifiedCloudflareCaptcha", + title="Passed Cloudflare Captcha", + type="VER", + ) + + +def create_tokentap_constraint(apps, schema_editor): + Constraint = apps.get_model("tokenTap", "Constraint") + + Constraint.objects.create( + name="core.HasVerifiedCloudflareCaptcha", + description="HasVerifiedCloudflareCaptcha", + title="Passed Cloudflare Captcha", + type="VER", + ) + + class Migration(migrations.Migration): dependencies = [ - ('prizetap', '0075_alter_constraint_name'), + ("prizetap", "0075_alter_constraint_name"), ] operations = [ migrations.AlterField( - model_name='constraint', - name='name', - field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('prizetap.HaveUnitapPass', 'HaveUnitapPass'), ('prizetap.NotHaveUnitapPass', 'NotHaveUnitapPass'), ('faucet.OptimismDonationConstraint', 'OptimismDonationConstraint'), ('faucet.OptimismClaimingGasConstraint', 'OptimismClaimingGasConstraint')], max_length=255, unique=True), + model_name="constraint", + name="name", + field=models.CharField( + choices=[ + ("core.BrightIDMeetVerification", "BrightIDMeetVerification"), + ("core.BrightIDAuraVerification", "BrightIDAuraVerification"), + ("core.HasNFTVerification", "HasNFTVerification"), + ("core.HasTokenVerification", "HasTokenVerification"), + ( + "core.HasTokenTransferVerification", + "HasTokenTransferVerification", + ), + ("core.AllowListVerification", "AllowListVerification"), + ("core.HasENSVerification", "HasENSVerification"), + ("core.HasLensProfile", "HasLensProfile"), + ("core.IsFollowingLensUser", "IsFollowingLensUser"), + ("core.BeFollowedByLensUser", "BeFollowedByLensUser"), + ("core.DidMirrorOnLensPublication", "DidMirrorOnLensPublication"), + ("core.DidCollectLensPublication", "DidCollectLensPublication"), + ("core.HasMinimumLensPost", "HasMinimumLensPost"), + ("core.HasMinimumLensFollower", "HasMinimumLensFollower"), + ("core.BeFollowedByFarcasterUser", "BeFollowedByFarcasterUser"), + ("core.HasMinimumFarcasterFollower", "HasMinimumFarcasterFollower"), + ("core.DidLikedFarcasterCast", "DidLikedFarcasterCast"), + ("core.DidRecastFarcasterCast", "DidRecastFarcasterCast"), + ("core.IsFollowingFarcasterUser", "IsFollowingFarcasterUser"), + ("core.HasFarcasterProfile", "HasFarcasterProfile"), + ("core.BeAttestedBy", "BeAttestedBy"), + ("core.Attest", "Attest"), + ("core.HasDonatedOnGitcoin", "HasDonatedOnGitcoin"), + ("core.HasMinimumHumanityScore", "HasMinimumHumanityScore"), + ("core.HasGitcoinPassportProfile", "HasGitcoinPassportProfile"), + ("core.IsFollowingFarcasterChannel", "IsFollowingFarcasterChannel"), + ("core.BridgeEthToArb", "BridgeEthToArb"), + ("core.IsFollowinTwitterUser", "IsFollowinTwitterUser"), + ("core.BeFollowedByTwitterUser", "BeFollowedByTwitterUser"), + ("core.DidRetweetTweet", "DidRetweetTweet"), + ("core.DidQuoteTweet", "DidQuoteTweet"), + ("core.HasMuonNode", "HasMuonNode"), + ("core.DelegateArb", "DelegateArb"), + ("core.DelegateOP", "DelegateOP"), + ("core.DidDelegateArbToAddress", "DidDelegateArbToAddress"), + ("core.DidDelegateOPToAddress", "DidDelegateOPToAddress"), + ("core.GLMStakingVerification", "GLMStakingVerification"), + ("core.IsFollowingTwitterBatch", "IsFollowingTwitterBatch"), + ("core.IsFollowingFarcasterBatch", "IsFollowingFarcasterBatch"), + ( + "core.HasVerifiedCloudflareCaptcha", + "HasVerifiedCloudflareCaptcha", + ), + ("prizetap.HaveUnitapPass", "HaveUnitapPass"), + ("prizetap.NotHaveUnitapPass", "NotHaveUnitapPass"), + ("faucet.OptimismDonationConstraint", "OptimismDonationConstraint"), + ( + "faucet.OptimismClaimingGasConstraint", + "OptimismClaimingGasConstraint", + ), + ], + max_length=255, + unique=True, + ), ), + migrations.RunPython(create_tokentap_constraint), + migrations.RunPython(create_prizetap_constraint), ] From 7b953cf4467aa98cc1bbc70c11d1be2a8b78a561 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 13:58:23 +0330 Subject: [PATCH 029/110] removed extra implementation of cloudflare image field to use a custom package upgrade django to 5 and other dependencies refactored deprecated utc import --- brightIDfaucet/settings.py | 10 ++ core/fields.py | 73 --------- core/services.py | 97 ------------ core/storages.py | 138 ------------------ ...0019_brightuser__last_verified_datetime.py | 4 +- prizetap/models.py | 3 +- requirements.txt | 7 +- ...ve_tokendistribution_image_url_and_more.py | 23 +++ tokenTap/models.py | 6 +- 9 files changed, 45 insertions(+), 316 deletions(-) delete mode 100644 core/services.py delete mode 100644 core/storages.py create mode 100644 tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 86d6db1..93323eb 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -166,6 +166,16 @@ def before_send(event, hint): WSGI_APPLICATION = "brightIDfaucet.wsgi.application" +STORAGES = { + "default": { + "BACKEND": "cloudflare_images.storage.CloudflareImagesStorage", + }, + "staticfiles": { # default + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +} + + # Database DATABASES = {"default": dj_database_url.config(conn_max_age=600)} diff --git a/core/fields.py b/core/fields.py index 4262ae1..d2eb73e 100644 --- a/core/fields.py +++ b/core/fields.py @@ -3,82 +3,9 @@ such as variants in Cloudflare Images """ -from django.db.models.base import Model -from django.db.models.fields.files import ( - FileField, - ImageFieldFile, - ImageField, - ImageFileDescriptor, -) -from .storages import CloudflareImagesStorage from django.db import models -class CloudflareImagesFileDescriptor(ImageFileDescriptor): - """ - Inherits ImageField's descriptor class - """ - - pass - - -class CloudflareImagesFieldFile(ImageFieldFile): - """ - Inherits ImageField's attr class - """ - storage: CloudflareImagesStorage - - def __init__(self, instance: Model, field: FileField, name: str | None) -> None: - super().__init__(instance, field, name) - - self.storage = CloudflareImagesStorage() - - @property - def url(self): - """ - Overriding the default url method to pass our variant - """ - return self.storage.url_with_variant(self.name, variant=self.field.variant) # type: ignore - - -class CloudflareImagesField(ImageField): - """ - Custom field based on ImageField allowing us to pass a variant - """ - - attr_class = CloudflareImagesFieldFile - descriptor_class = CloudflareImagesFileDescriptor - description = "Image" - storage = CloudflareImagesStorage - - def __init__( - self, - verbose_name=None, - name=None, - width_field=None, - height_field=None, - variant=None, - **kwargs, - ): - """ - Calling ImageFieldFile constructor and setting our variant - """ - self.variant = variant or "public" - super().__init__(verbose_name, name, width_field, height_field, **kwargs) - - def deconstruct(self): - """ - Returns the deconstructed version of our field. - Same as ImageField with variant on top - """ - name, path, args, kwargs = super().deconstruct() - kwargs["variant"] = self.variant - return name, path, args, kwargs - - - - - class BigNumField(models.Field): empty_strings_allowed = False diff --git a/core/services.py b/core/services.py deleted file mode 100644 index 54af4bb..0000000 --- a/core/services.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Contains the Cloudflare Image service which handles the API exchanges -""" -from django.conf import settings - -import requests - - -class ApiException(Exception): - """ - Exception raised by Cloudflare Images API - """ - - pass - - -class CloudflareImagesService: - """ - API client for Cloudflare Images - """ - - def __init__(self): - """ - Loads the configuration - """ - self.account_id = settings.CLOUDFLARE_IMAGES_ACCOUNT_ID - self.api_token = settings.CLOUDFLARE_IMAGES_API_TOKEN - self.account_hash = settings.CLOUDFLARE_IMAGES_ACCOUNT_HASH - self.api_timeout = 60 - self.domain = None - - def upload(self, file): - """ - Uploads a file and return its name, otherwise raise an exception - """ - url = "https://api.cloudflare.com/client/v4/accounts/{}/images/v1".format( - self.account_id - ) - - headers = {"Authorization": "Bearer {}".format(self.api_token)} - - files = {"file": file} - - response = requests.post( - url, headers=headers, timeout=self.api_timeout, files=files - ) - - status_code = response.status_code - if status_code != 200: - raise ApiException(response.content) - - response_body = response.json() - return response_body.get("result").get("id") - - def get_url(self, name, variant): - """ - Returns the public URL for the given image ID - """ - - if self.domain: - return f"https://{self.domain}/cdn-cgi/imagedelivery/{self.account_hash}/{name}/{variant}" - - return f"https://imagedelivery.net/{self.account_hash}/{name}/{variant}" - - def open(self, name, variant=None): - """ - Retrieves a file and return its content, otherwise raise an exception - """ - - url = self.get_url(name, variant or "public") - - response = requests.get(url, timeout=self.api_timeout) - - status_code = response.status_code - if status_code != 200: - raise ApiException(response.content) - - return response.content - - def delete(self, name): - """ - Deletes a file if it exists, otherwise raise an exception - """ - - url = "https://api.cloudflare.com/client/v4/accounts/{}/images/v1/{}".format( - self.account_id, name - ) - - headers = {"Authorization": "Bearer {}".format(self.api_token)} - - response = requests.delete( - url, timeout=self.api_timeout, headers=headers - ) - - status_code = response.status_code - if status_code != 200: - raise ApiException(str(response.text)) \ No newline at end of file diff --git a/core/storages.py b/core/storages.py deleted file mode 100644 index 658ed13..0000000 --- a/core/storages.py +++ /dev/null @@ -1,138 +0,0 @@ -""" -Contains the Cloudflare Image storage which is supposed to replace Django's -default Storage (see README.md) -Django's default storage class: https://github.com/django/django/blob/main/django/core/files/storage.py -""" - -from django.core.files.base import File -from django.core.files.storage import Storage -from django.utils.deconstruct import deconstructible -from .services import CloudflareImagesService - - -@deconstructible -class CloudflareImagesStorage(Storage): - """ - Django storage for Cloudflare Images - """ - - def __init__(self): - """ - Setups the storage - """ - super().__init__() - - self.service = CloudflareImagesService() - - def _open(self, name, mode="rb"): - """ - Returns the image as a File - The parameter "mode" has been kept to respect the original signature - (and it fails without it) but it wont have any impact - Has to be implemented. - """ - content = self.service.open(name) - return File(content, name=name) - - def _save(self, name, content): - """ - Tries to upload the file and return its name - Has to be implemented. - """ - new_name = self.generate_filename(name) - content.name = new_name - return self.service.upload(content) - - def get_valid_name(self, name): - """ - Returns a valid name for the file. - Has to be implemented. - """ - return name - - def get_available_name(self, name, max_length=None): - """ - Returns the available name for the file. - Has to be implemented. - """ - return self.generate_filename(name) - - def generate_filename(self, filename): - """ - Returns the name of the file. - Has to be implemented. - """ - return filename - - def delete(self, name): - """ - Tries to delete the specified file from the storage system. - Has to be implemented. - """ - self.service.delete(name) - - def exists(self, name): - """ - Return True if a file referenced by the given name already exists in the - storage system, or False if the name is available for a new file. - """ - raise NotImplementedError( - "subclasses of Storage must provide an exists() method" - ) - - def listdir(self, path): - """ - List the contents of the specified path. Return a 2-tuple of lists: - the first item being directories, the second item being files. - """ - raise NotImplementedError( - "subclasses of Storage must provide a listdir() method" - ) - - def size(self, name): - """ - Return the total size, in bytes, of the file specified by name. - """ - content = self.service.open(name) - return len(content) - - def url(self, name): - """ - Return an absolute URL where the file's contents can be accessed - directly by a web browser. - Has to be implemented. - """ - return self.url_with_variant(name, 'public') - - def url_with_variant(self, name, variant): - """ - Custom methods which allow to pass a variant and respect the original signature of `url` - """ - return self.service.get_url(name, variant) - - def get_accessed_time(self, name): - """ - Return the last accessed time (as a datetime) of the file specified by - name. The datetime will be timezone-aware if USE_TZ=True. - """ - raise NotImplementedError( - "subclasses of Storage must provide a get_accessed_time() method" - ) - - def get_created_time(self, name): - """ - Return the creation time (as a datetime) of the file specified by name. - The datetime will be timezone-aware if USE_TZ=True. - """ - raise NotImplementedError( - "subclasses of Storage must provide a get_created_time() method" - ) - - def get_modified_time(self, name): - """ - Return the last modified time (as a datetime) of the file specified by - name. The datetime will be timezone-aware if USE_TZ=True. - """ - raise NotImplementedError( - "subclasses of Storage must provide a get_modified_time() method" - ) \ No newline at end of file diff --git a/faucet/migrations/0019_brightuser__last_verified_datetime.py b/faucet/migrations/0019_brightuser__last_verified_datetime.py index 6f4e85f..34284c7 100644 --- a/faucet/migrations/0019_brightuser__last_verified_datetime.py +++ b/faucet/migrations/0019_brightuser__last_verified_datetime.py @@ -2,7 +2,7 @@ import datetime from django.db import migrations, models -from django.utils.timezone import utc + class Migration(migrations.Migration): @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='brightuser', name='_last_verified_datetime', - field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=utc)), + field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)), ), ] diff --git a/prizetap/models.py b/prizetap/models.py index 8b9b548..315fe35 100644 --- a/prizetap/models.py +++ b/prizetap/models.py @@ -6,6 +6,7 @@ from authentication.models import UserProfile from core.models import BigNumField, Chain, UserConstraint from faucet.constraints import OptimismClaimingGasConstraint, OptimismDonationConstraint +from cloudflare_images.field import CloudflareImagesField from .constraints import HaveUnitapPass, NotHaveUnitapPass @@ -49,7 +50,7 @@ class Meta: twitter_url = models.URLField(max_length=255, null=True, blank=True) email_url = models.EmailField(max_length=255) telegram_url = models.URLField(max_length=255, null=True, blank=True) - image_url = models.URLField(max_length=255, null=True, blank=True) + image = CloudflareImagesField(max_length=255, null=True, blank=True) prize_amount = BigNumField() prize_asset = models.CharField(max_length=255) diff --git a/requirements.txt b/requirements.txt index 6f6be00..c1f7e3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ bip-utils==2.3.0 -django==4.0.4 +django==5 anchorpy==0.15.0 -djangorestframework==3.13.1 +djangorestframework==3.15.2 djangorestframework-camel-case==1.3.0 dj-database-url==0.5.0 django-celery-results==2.3.1 @@ -11,7 +11,7 @@ django-encrypted-model-fields==0.6.5 django-filter==21.1 django-polymorphic==3.1.0 django-rest-polymorphic==0.1.10 -drf-yasg==1.20.0 +drf-yasg~=1.21 ed25519==1.5 psycopg2-binary==2.9.3 gunicorn==20.1.0 @@ -33,3 +33,4 @@ django-safedelete~=1.3.3 tweepy~=4.14.0 ratelimit~=2.2.1 pillow==10.4.0 +django-cloudflare-images~=0.6.0 \ No newline at end of file diff --git a/tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py b/tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py new file mode 100644 index 0000000..36cf838 --- /dev/null +++ b/tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0 on 2024-08-26 10:22 + +import cloudflare_images.field +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0061_alter_constraint_name'), + ] + + operations = [ + migrations.RemoveField( + model_name='tokendistribution', + name='image_url', + ), + migrations.AddField( + model_name='tokendistribution', + name='image', + field=cloudflare_images.field.CloudflareImagesField(blank=True, upload_to='', variant='custom'), + ), + ] diff --git a/tokenTap/models.py b/tokenTap/models.py index 81be367..f072619 100644 --- a/tokenTap/models.py +++ b/tokenTap/models.py @@ -12,6 +12,8 @@ from faucet.constraints import OptimismHasClaimedGasConstraint from faucet.models import ClaimReceipt from tokenTap.constants import UNITAP_PASS_CLAIM_PERCENT +from cloudflare_images.field import CloudflareImagesField + from .constraints import ( OnceInALifeTimeVerification, @@ -47,8 +49,8 @@ class Status(models.TextChoices): twitter_url = models.URLField(max_length=255, null=True, blank=True) email_url = models.EmailField(max_length=255) telegram_url = models.URLField(max_length=255, null=True, blank=True) - image_url = models.URLField(max_length=255, null=True, blank=True) - token_image_url = models.URLField(max_length=255, null=True, blank=True) + image = CloudflareImagesField(variant="public", blank=True) + token_image = CloudflareImagesField(variant="public", blank=True) token = models.CharField(max_length=100) token_address = models.CharField(max_length=255) From e26ca71e42734a9c9f2aed8265a24ddfd3bd5184 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 14:50:03 +0330 Subject: [PATCH 030/110] fixed arguments passing for is_valid --- authentication/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/authentication/serializers.py b/authentication/serializers.py index 503e77d..976c337 100644 --- a/authentication/serializers.py +++ b/authentication/serializers.py @@ -46,7 +46,7 @@ class Meta: fields = ["pk", "wallet_type", "address", "signature", "message"] def is_valid(self, raise_exception=False): - super_is_validated = super().is_valid(raise_exception) + super_is_validated = super().is_valid(raise_exception=raise_exception) address = self.validated_data.get("address") message = self.validated_data.get("message") @@ -208,8 +208,8 @@ class Meta: ] def is_valid(self, raise_exception=False): - super_is_validated = super().is_valid(raise_exception) - is_address_valid = self.validate_address(raise_exception) + super_is_validated = super().is_valid(raise_exception=raise_exception) + is_address_valid = self.validate_address(raise_exception=raise_exception) return is_address_valid and super_is_validated @@ -237,7 +237,7 @@ class Meta: ] def is_valid(self, raise_exception=False): - super_is_validated = super().is_valid(raise_exception) + super_is_validated = super().is_valid(raise_exception=raise_exception) is_address_valid = self.validate_address(raise_exception) return is_address_valid and super_is_validated @@ -255,7 +255,7 @@ class Meta: ] def is_valid(self, raise_exception=False): - super_is_validated = super().is_valid(raise_exception) + super_is_validated = super().is_valid(raise_exception=raise_exception) is_address_valid = self.validate_address(raise_exception) return is_address_valid and super_is_validated @@ -273,7 +273,7 @@ class Meta: ] def is_valid(self, raise_exception=False): - super_is_validated = super().is_valid(raise_exception) + super_is_validated = super().is_valid(raise_exception=raise_exception) is_address_valid = self.validate_address(raise_exception) return is_address_valid and super_is_validated From c42a1fff24e974a3dd2c9efe5eca6fbdc9cc04f0 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 14:59:00 +0330 Subject: [PATCH 031/110] fixed migrations error --- prizetap/migrations/0076_alter_constraint_name.py | 11 ----------- tokenTap/migrations/0062_alter_constraint_name.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/prizetap/migrations/0076_alter_constraint_name.py b/prizetap/migrations/0076_alter_constraint_name.py index 8eeae55..45504ca 100644 --- a/prizetap/migrations/0076_alter_constraint_name.py +++ b/prizetap/migrations/0076_alter_constraint_name.py @@ -14,16 +14,6 @@ def create_prizetap_constraint(apps, schema_editor): ) -def create_tokentap_constraint(apps, schema_editor): - Constraint = apps.get_model("tokenTap", "Constraint") - - Constraint.objects.create( - name="core.HasVerifiedCloudflareCaptcha", - description="HasVerifiedCloudflareCaptcha", - title="Passed Cloudflare Captcha", - type="VER", - ) - class Migration(migrations.Migration): @@ -95,6 +85,5 @@ class Migration(migrations.Migration): unique=True, ), ), - migrations.RunPython(create_tokentap_constraint), migrations.RunPython(create_prizetap_constraint), ] diff --git a/tokenTap/migrations/0062_alter_constraint_name.py b/tokenTap/migrations/0062_alter_constraint_name.py index 06f2b7e..1ba8a10 100644 --- a/tokenTap/migrations/0062_alter_constraint_name.py +++ b/tokenTap/migrations/0062_alter_constraint_name.py @@ -3,6 +3,18 @@ from django.db import migrations, models + +def create_tokentap_constraint(apps, schema_editor): + Constraint = apps.get_model("tokenTap", "Constraint") + + Constraint.objects.create( + name="core.HasVerifiedCloudflareCaptcha", + description="HasVerifiedCloudflareCaptcha", + title="Passed Cloudflare Captcha", + type="VER", + ) + + class Migration(migrations.Migration): dependencies = [ @@ -15,4 +27,5 @@ class Migration(migrations.Migration): name='name', field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('tokenTap.OncePerMonthVerification', 'OncePerMonthVerification'), ('tokenTap.OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('faucet.OptimismHasClaimedGasConstraint', 'OptimismHasClaimedGasConstraint')], max_length=255, unique=True), ), + migrations.RunPython(create_tokentap_constraint), ] From 512683fa3efc761c29ef0a4f019f17403b9bfcbd Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 15:01:56 +0330 Subject: [PATCH 032/110] updated requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0b10be9..ac0ef18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-celery-results==2.3.1 django-bmemcached==0.3.0 django-cors-headers==3.12.0 django-encrypted-model-fields==0.6.5 -django-filter==21.1 +django-filter==24.3 django-polymorphic==3.1.0 django-rest-polymorphic==0.1.10 drf-yasg==1.20.0 From 78ff0a3215c96de446ebaeab3957e7a88ac05ada Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 15:05:50 +0330 Subject: [PATCH 033/110] renamed request context variable to request --- core/constraints/captcha.py | 10 +++++----- .../0019_brightuser__last_verified_datetime.py | 3 +-- prizetap/validators.py | 4 ++-- prizetap/views.py | 4 ++-- tokenTap/validators.py | 4 ++-- tokenTap/views.py | 4 ++-- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index d775109..feac1eb 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -16,17 +16,17 @@ class HasVerifiedCloudflareCaptcha(ConstraintVerification): def is_observed(self, *args, **kwargs) -> bool: - if self.context is None or self.context.get("request_context") is None: + if self.context is None or self.context.get("requset") is None: return False cloudflare = CloudflareUtil() - request: RequestContextExtractor = RequestContextExtractor( - self.context["request_context"] + request_context: RequestContextExtractor = RequestContextExtractor( + self.context["requset"] ) - turnstile_token = request.data.get("cf-turnstile-response") + turnstile_token = request_context.data.get("cf-turnstile-response") return turnstile_token is not None and cloudflare.is_verified( - turnstile_token, request.ip + turnstile_token, request_context.ip ) diff --git a/faucet/migrations/0019_brightuser__last_verified_datetime.py b/faucet/migrations/0019_brightuser__last_verified_datetime.py index 6f4e85f..f2aac0b 100644 --- a/faucet/migrations/0019_brightuser__last_verified_datetime.py +++ b/faucet/migrations/0019_brightuser__last_verified_datetime.py @@ -2,7 +2,6 @@ import datetime from django.db import migrations, models -from django.utils.timezone import utc class Migration(migrations.Migration): @@ -15,6 +14,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='brightuser', name='_last_verified_datetime', - field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=utc)), + field=models.DateTimeField(default=datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)), ), ] diff --git a/prizetap/validators.py b/prizetap/validators.py index 7a4015f..3d8e37f 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -15,7 +15,7 @@ def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] self.raffle: Raffle = kwargs["raffle"] self.raffle_data: dict = kwargs.get("raffle_data", dict()) - self.request_context = kwargs.get("request_context") + self.request = kwargs.get("requset") def can_enroll_in_raffle(self): if not self.raffle.is_claimable: @@ -30,7 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, context={"request_context": self.request_context} + self.user_profile, context={"request": self.request} ) constraint.response = c.response try: diff --git a/prizetap/views.py b/prizetap/views.py index 18c354e..cdcb4d2 100644 --- a/prizetap/views.py +++ b/prizetap/views.py @@ -85,7 +85,7 @@ def post(self, request, pk): user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, - request_context=request, + request=request, ) validator.is_valid(self.request.data) @@ -198,7 +198,7 @@ def get(self, request, raffle_pk): user_profile=user_profile, raffle=raffle, raffle_data=raffle_data, - request_context=request, + request=request, ) validated_constraints = validator.check_user_constraints(raise_exception=False) diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 1b555de..315e464 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -48,7 +48,7 @@ def __init__( self.td = td self.td_data = td_data self.user_profile = user_profile - self.request_context = kwargs.get("request_context") + self.request = kwargs.get("request") def check_user_permissions(self, raise_exception=True): try: @@ -61,7 +61,7 @@ def check_user_permissions(self, raise_exception=True): for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( self.user_profile, - context={"request_context": self.request_context} + context={"request": self.request} ) constraint.response = c.response try: diff --git a/tokenTap/views.py b/tokenTap/views.py index a979cfd..8a60af8 100644 --- a/tokenTap/views.py +++ b/tokenTap/views.py @@ -136,7 +136,7 @@ def post(self, request, *args, **kwargs): token_distribution, user_profile, td_data, - request_context=request, + request=request, ) validator.is_valid() @@ -188,7 +188,7 @@ def get(self, request, td_id): response_constraints = [] validator = TokenDistributionValidator( - td, user_profile, td_data, request_context=request + td, user_profile, td_data, request=request ) validated_constraints = validator.check_user_permissions(raise_exception=False) for c_pk, data in validated_constraints.items(): From faacb848bcfddf6e399f906560ce9fec2e084468 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 15:09:09 +0330 Subject: [PATCH 034/110] fixed field typos --- prizetap/serializers.py | 2 +- tokenTap/serializers.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/prizetap/serializers.py b/prizetap/serializers.py index 5c89de0..9e0ee46 100644 --- a/prizetap/serializers.py +++ b/prizetap/serializers.py @@ -161,7 +161,7 @@ class Meta: "twitter_url", "email_url", "telegram_url", - "image_url", + "image", "prize_amount", "prize_asset", "prize_name", diff --git a/tokenTap/serializers.py b/tokenTap/serializers.py index 4a5708c..b4d13d3 100644 --- a/tokenTap/serializers.py +++ b/tokenTap/serializers.py @@ -54,8 +54,8 @@ class Meta: "twitter_url", "email_url", "telegram_url", - "image_url", - "token_image_url", + "image", + "token_image", "token", "token_address", "decimals", @@ -124,7 +124,7 @@ class Meta: "distributor_url", "discord_url", "twitter_url", - "image_url", + "image", "token", "token_address", "decimals", @@ -139,7 +139,7 @@ class Meta: "deadline", "max_number_of_claims", "notes", - "token_image_url", + "token_image", "claim_deadline_for_unitap_pass_user", "max_claim_number_for_unitap_pass_user", "remaining_claim_for_unitap_pass_user", From db058383f27693f5dacf853a019045a8343a1095 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 15:13:14 +0330 Subject: [PATCH 035/110] updated requirements --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c1f7e3a..924296c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,9 @@ djangorestframework-camel-case==1.3.0 dj-database-url==0.5.0 django-celery-results==2.3.1 django-bmemcached==0.3.0 -django-cors-headers==3.12.0 +django-cors-headers==4.4.0 django-encrypted-model-fields==0.6.5 -django-filter==21.1 +django-filter==24.3 django-polymorphic==3.1.0 django-rest-polymorphic==0.1.10 drf-yasg~=1.21 From ad76e29f0fb9aada2816289be6334239314ce318 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 15:14:13 +0330 Subject: [PATCH 036/110] fixed requirements version error --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac0ef18..0b10be9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-celery-results==2.3.1 django-bmemcached==0.3.0 django-cors-headers==3.12.0 django-encrypted-model-fields==0.6.5 -django-filter==24.3 +django-filter==21.1 django-polymorphic==3.1.0 django-rest-polymorphic==0.1.10 drf-yasg==1.20.0 From 4a6303ca26e797739f88f94eba6bd51bb6c73c3a Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 15:26:31 +0330 Subject: [PATCH 037/110] added migrations for image fields --- ...76_remove_raffle_image_url_raffle_image.py | 19 +++++++++++++++ ...endistribution_token_image_url_and_more.py | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 prizetap/migrations/0076_remove_raffle_image_url_raffle_image.py create mode 100644 tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py diff --git a/prizetap/migrations/0076_remove_raffle_image_url_raffle_image.py b/prizetap/migrations/0076_remove_raffle_image_url_raffle_image.py new file mode 100644 index 0000000..87a35d7 --- /dev/null +++ b/prizetap/migrations/0076_remove_raffle_image_url_raffle_image.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0 on 2024-08-26 11:54 + +import cloudflare_images.field +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0075_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='raffle', + name='image_url', + field=cloudflare_images.field.CloudflareImagesField(blank=True, max_length=255, null=True, upload_to='', variant='public'), + ), + ] \ No newline at end of file diff --git a/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py b/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py new file mode 100644 index 0000000..1cd9a6e --- /dev/null +++ b/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0 on 2024-08-26 11:54 + +import cloudflare_images.field +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0062_remove_tokendistribution_image_url_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='tokendistribution', + name='token_image_url', + field=cloudflare_images.field.CloudflareImagesField(blank=True, upload_to='', variant='public'), + ), + migrations.AlterField( + model_name='tokendistribution', + name='image', + field=cloudflare_images.field.CloudflareImagesField(blank=True, upload_to='', variant='public'), + ), + ] From 7ccec3d5a49dc02fc4de269baff6b0a85c5fb6d4 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 20:22:41 +0330 Subject: [PATCH 038/110] fixed passing context argument to is_observed --- core/constraints/abstract.py | 3 +-- core/constraints/captcha.py | 8 +++++--- prizetap/validators.py | 4 ++-- tokenTap/validators.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index e0f70f9..5292c94 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -56,10 +56,9 @@ class ConstraintVerification(ABC): app_name = ConstraintApp.GENERAL.value __response_text = "" - def __init__(self, user_profile, context=None) -> None: + def __init__(self, user_profile) -> None: self.user_profile = user_profile self._param_values = {} - self.context = context def get_info(self, *args, **kwargs): pass diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index feac1eb..d75cc76 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -16,17 +16,19 @@ class HasVerifiedCloudflareCaptcha(ConstraintVerification): def is_observed(self, *args, **kwargs) -> bool: - if self.context is None or self.context.get("requset") is None: + context = kwargs.get("context") + + if context is None or context.get("requset") is None: return False cloudflare = CloudflareUtil() request_context: RequestContextExtractor = RequestContextExtractor( - self.context["requset"] + context["requset"] ) turnstile_token = request_context.data.get("cf-turnstile-response") - return turnstile_token is not None and cloudflare.is_verified( + return request_context.ip is not None and turnstile_token is not None and cloudflare.is_verified( turnstile_token, request_context.ip ) diff --git a/prizetap/validators.py b/prizetap/validators.py index 3d8e37f..d636658 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -30,7 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, context={"request": self.request} + self.user_profile ) constraint.response = c.response try: @@ -56,7 +56,7 @@ def check_user_constraints(self, raise_exception=True): ) else: is_verified = constraint.is_observed( - **cdata, from_time=int(self.raffle.start_at.timestamp()) + **cdata, from_time=int(self.raffle.start_at.timestamp()), context={"request": self.request} ) caching_time = 60 * 60 if is_verified else 60 expiration_time = time.time() + caching_time diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 315e464..3ee9ae3 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -61,7 +61,6 @@ def check_user_permissions(self, raise_exception=True): for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( self.user_profile, - context={"request": self.request} ) constraint.response = c.response try: @@ -85,6 +84,7 @@ def check_user_permissions(self, raise_exception=True): is_verified = constraint.is_observed( **cdata, token_distribution=self.td, + context={"request": self.request} ) caching_time = 60 * 60 if is_verified else 60 expiration_time = time.time() + caching_time From 4137c927969bea8a4901033d53c0edb2daddaa01 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 20:28:01 +0330 Subject: [PATCH 039/110] fixed migrations --- ...7_remove_raffle_image_url_raffle_image.py} | 7 +++++- ...ve_tokendistribution_image_url_and_more.py | 23 ------------------- ...endistribution_token_image_url_and_more.py | 14 +++++++++-- 3 files changed, 18 insertions(+), 26 deletions(-) rename prizetap/migrations/{0076_remove_raffle_image_url_raffle_image.py => 0077_remove_raffle_image_url_raffle_image.py} (69%) delete mode 100644 tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py diff --git a/prizetap/migrations/0076_remove_raffle_image_url_raffle_image.py b/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py similarity index 69% rename from prizetap/migrations/0076_remove_raffle_image_url_raffle_image.py rename to prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py index 87a35d7..4843694 100644 --- a/prizetap/migrations/0076_remove_raffle_image_url_raffle_image.py +++ b/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py @@ -7,10 +7,15 @@ class Migration(migrations.Migration): dependencies = [ - ('prizetap', '0075_alter_constraint_name'), + ('prizetap', '0076_alter_constraint_name'), ] operations = [ + migrations.RenameField( + model_name='raffle', + old_name='image_url', + new_name='image', + ), migrations.AlterField( model_name='raffle', name='image_url', diff --git a/tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py b/tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py deleted file mode 100644 index 36cf838..0000000 --- a/tokenTap/migrations/0062_remove_tokendistribution_image_url_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0 on 2024-08-26 10:22 - -import cloudflare_images.field -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tokenTap', '0061_alter_constraint_name'), - ] - - operations = [ - migrations.RemoveField( - model_name='tokendistribution', - name='image_url', - ), - migrations.AddField( - model_name='tokendistribution', - name='image', - field=cloudflare_images.field.CloudflareImagesField(blank=True, upload_to='', variant='custom'), - ), - ] diff --git a/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py b/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py index 1cd9a6e..4cac9b8 100644 --- a/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py +++ b/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py @@ -7,14 +7,24 @@ class Migration(migrations.Migration): dependencies = [ - ('tokenTap', '0062_remove_tokendistribution_image_url_and_more'), + ('tokenTap', '0062_alter_constraint_name'), ] operations = [ + migrations.RenameField( + model_name='tokendistribution', + old_name='token_image_url', + new_name='token_image', + ), migrations.AlterField( model_name='tokendistribution', - name='token_image_url', + name='token_image', field=cloudflare_images.field.CloudflareImagesField(blank=True, upload_to='', variant='public'), + ), + migrations.RenameField( + model_name='tokendistribution', + old_name='image_url', + new_name='image', ), migrations.AlterField( model_name='tokendistribution', From cf505af825ce31a02f2ac50bc4950b3502a4d098 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 20:31:00 +0330 Subject: [PATCH 040/110] fixed typo error --- core/constraints/captcha.py | 4 ++-- prizetap/validators.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index d75cc76..458ce4c 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -18,13 +18,13 @@ def is_observed(self, *args, **kwargs) -> bool: context = kwargs.get("context") - if context is None or context.get("requset") is None: + if context is None or context.get("request") is None: return False cloudflare = CloudflareUtil() request_context: RequestContextExtractor = RequestContextExtractor( - context["requset"] + context["request"] ) turnstile_token = request_context.data.get("cf-turnstile-response") diff --git a/prizetap/validators.py b/prizetap/validators.py index d636658..e0ac8f9 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -15,7 +15,7 @@ def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] self.raffle: Raffle = kwargs["raffle"] self.raffle_data: dict = kwargs.get("raffle_data", dict()) - self.request = kwargs.get("requset") + self.request = kwargs.get("request") def can_enroll_in_raffle(self): if not self.raffle.is_claimable: From 167035bdf1885959e153abb4677672ab502a50cd Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 21:04:06 +0330 Subject: [PATCH 041/110] added is_cachable to requirements --- core/constraints/abstract.py | 1 + core/constraints/captcha.py | 1 + prizetap/validators.py | 26 ++++++++++++++------------ tokenTap/validators.py | 26 ++++++++++++++------------ 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index 5292c94..3e967f8 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -55,6 +55,7 @@ class ConstraintVerification(ABC): _param_keys = [] app_name = ConstraintApp.GENERAL.value __response_text = "" + is_cachable = True def __init__(self, user_profile) -> None: self.user_profile = user_profile diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index 458ce4c..f2b68aa 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -13,6 +13,7 @@ class HasVerifiedCloudflareCaptcha(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.GENERAL.value + is_cachable = False def is_observed(self, *args, **kwargs) -> bool: diff --git a/prizetap/validators.py b/prizetap/validators.py index e0ac8f9..22c2599 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -58,18 +58,20 @@ def check_user_constraints(self, raise_exception=True): is_verified = constraint.is_observed( **cdata, from_time=int(self.raffle.start_at.timestamp()), context={"request": self.request} ) - caching_time = 60 * 60 if is_verified else 60 - expiration_time = time.time() + caching_time - cache_data = { - "is_verified": is_verified, - "info": info, - "expiration_time": expiration_time, - } - cache.set( - cache_key, - cache_data, - caching_time, - ) + if constraint.is_cachable: + caching_time = 60 * 60 if is_verified else 60 + expiration_time = time.time() + caching_time + cache_data = { + "is_verified": is_verified, + "info": info, + "expiration_time": expiration_time, + } + cache.set( + cache_key, + cache_data, + caching_time, + ) + if not cache_data.get("is_verified"): error_messages[c.title] = constraint.response result[c.pk] = cache_data diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 3ee9ae3..76a4eaa 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -86,18 +86,20 @@ def check_user_permissions(self, raise_exception=True): token_distribution=self.td, context={"request": self.request} ) - caching_time = 60 * 60 if is_verified else 60 - expiration_time = time.time() + caching_time - cache_data = { - "is_verified": is_verified, - "info": info, - "expiration_time": expiration_time, - } - cache.set( - cache_key, - cache_data, - caching_time, - ) + if constraint.is_cachable: + caching_time = 60 * 60 if is_verified else 60 + expiration_time = time.time() + caching_time + cache_data = { + "is_verified": is_verified, + "info": info, + "expiration_time": expiration_time, + } + + cache.set( + cache_key, + cache_data, + caching_time, + ) if not cache_data.get("is_verified"): error_messages[c.title] = constraint.response result[c.pk] = cache_data From fbef1f86c57fe84e01c330e68e1324ebe62dd4ce Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 21:11:37 +0330 Subject: [PATCH 042/110] fixed errors on referencing variable --- prizetap/validators.py | 20 ++++++++++++-------- tokenTap/validators.py | 17 +++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/prizetap/validators.py b/prizetap/validators.py index 22c2599..7764c1b 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -58,20 +58,24 @@ def check_user_constraints(self, raise_exception=True): is_verified = constraint.is_observed( **cdata, from_time=int(self.raffle.start_at.timestamp()), context={"request": self.request} ) + + caching_time = 60 * 60 if is_verified else 60 + expiration_time = time.time() + caching_time + cache_data = { + "is_verified": is_verified, + "info": info, + "expiration_time": expiration_time, + } + if constraint.is_cachable: - caching_time = 60 * 60 if is_verified else 60 - expiration_time = time.time() + caching_time - cache_data = { - "is_verified": is_verified, - "info": info, - "expiration_time": expiration_time, - } + + cache.set( cache_key, cache_data, caching_time, ) - + if not cache_data.get("is_verified"): error_messages[c.title] = constraint.response result[c.pk] = cache_data diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 76a4eaa..58028b2 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -86,15 +86,16 @@ def check_user_permissions(self, raise_exception=True): token_distribution=self.td, context={"request": self.request} ) + + caching_time = 60 * 60 if is_verified else 60 + expiration_time = time.time() + caching_time + cache_data = { + "is_verified": is_verified, + "info": info, + "expiration_time": expiration_time, + } + if constraint.is_cachable: - caching_time = 60 * 60 if is_verified else 60 - expiration_time = time.time() + caching_time - cache_data = { - "is_verified": is_verified, - "info": info, - "expiration_time": expiration_time, - } - cache.set( cache_key, cache_data, From de89062bcaf92d771017c835142d7ea98a635238 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 21:28:37 +0330 Subject: [PATCH 043/110] refactored caching constraints --- core/utils.py | 13 ++++++++++++- prizetap/validators.py | 27 ++++++++------------------- tokenTap/validators.py | 35 +++++++++++++++++------------------ 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/core/utils.py b/core/utils.py index 0375c00..873ac87 100644 --- a/core/utils.py +++ b/core/utils.py @@ -397,4 +397,15 @@ def get_client_ip(x_forwarded_for): return ip return None - \ No newline at end of file + + +def cache_constraint_result(cache_key, is_verified, info): + caching_time = 60 * 60 if is_verified else 60 + expiration_time = time.time() + caching_time + cache_data = { + "is_verified": is_verified, + "info": info, + "expiration_time": expiration_time, + } + cache.set(cache_key, cache_data, caching_time) + return cache_data \ No newline at end of file diff --git a/prizetap/validators.py b/prizetap/validators.py index 7764c1b..9dc8abe 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -6,6 +6,7 @@ from authentication.models import UserProfile from core.constraints import ConstraintVerification, get_constraint +from core.utils import cache_constraint_result from .models import Raffle, RaffleEntry @@ -39,8 +40,8 @@ def check_user_constraints(self, raise_exception=True): pass cdata = self.raffle_data.get(str(c.pk), dict()) cache_key = f"prizetap-{self.user_profile.pk}-{self.raffle.pk}-{c.pk}" - cache_data = cache.get(cache_key) - if cache_data is None: + constraint_data = cache.get(cache_key) + if constraint_data is None: """ Refactor: this is not good design beacuse info is duplicated with is_observed so we need some design change for in is_observed so @@ -59,26 +60,14 @@ def check_user_constraints(self, raise_exception=True): **cdata, from_time=int(self.raffle.start_at.timestamp()), context={"request": self.request} ) - caching_time = 60 * 60 if is_verified else 60 - expiration_time = time.time() + caching_time - cache_data = { - "is_verified": is_verified, - "info": info, - "expiration_time": expiration_time, - } - if constraint.is_cachable: + constraint_data = cache_constraint_result(cache_key, is_verified, info) + else: + constraint_data = {"is_verified": is_verified, "info": info} - - cache.set( - cache_key, - cache_data, - caching_time, - ) - - if not cache_data.get("is_verified"): + if not constraint_data.get("is_verified"): error_messages[c.title] = constraint.response - result[c.pk] = cache_data + result[c.pk] = constraint_data if len(error_messages) and raise_exception: raise PermissionDenied(error_messages) return result diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 58028b2..a33698e 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -1,17 +1,19 @@ import json import logging -import time from django.core.cache import cache from rest_framework.exceptions import PermissionDenied from authentication.models import UserProfile from core.constraints import ConstraintVerification, get_constraint +from core.utils import cache_constraint_result from .helpers import has_credit_left from .models import ClaimReceipt, TokenDistribution + + class SetDistributionTxValidator: def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] @@ -36,6 +38,8 @@ def is_valid(self, data): raise PermissionDenied("Tx hash is not valid") + + class TokenDistributionValidator: def __init__( self, @@ -67,10 +71,11 @@ def check_user_permissions(self, raise_exception=True): constraint.param_values = param_values[c.name] except KeyError: pass + cdata = self.td_data.get(str(c.pk), dict()) cache_key = f"tokentap-{self.user_profile.pk}-{self.td.pk}-{c.pk}" - cache_data = cache.get(cache_key) - if cache_data is None: + constraint_data = cache.get(cache_key) + if constraint_data is None: info = constraint.get_info( **cdata, token_distribution=self.td, @@ -87,26 +92,20 @@ def check_user_permissions(self, raise_exception=True): context={"request": self.request} ) - caching_time = 60 * 60 if is_verified else 60 - expiration_time = time.time() + caching_time - cache_data = { - "is_verified": is_verified, - "info": info, - "expiration_time": expiration_time, - } - if constraint.is_cachable: - cache.set( - cache_key, - cache_data, - caching_time, - ) - if not cache_data.get("is_verified"): + constraint_data = cache_constraint_result(cache_key, is_verified, info) + else: + constraint_data = {"is_verified": is_verified, "info": info} + + if not constraint_data.get("is_verified"): error_messages[c.title] = constraint.response - result[c.pk] = cache_data + result[c.pk] = constraint_data if len(error_messages) and raise_exception: raise PermissionDenied(error_messages) return result + + def cache_constraint(self): + pass def check_user_credit(self): if self.td.is_one_time_claim: From fefa82b70ad0d9025eca25f2b0c5f99b9f8b424a Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 22:49:02 +0330 Subject: [PATCH 044/110] added valid and invalid cache until --- core/constraints/abstract.py | 2 ++ core/constraints/captcha.py | 4 +++- core/utils.py | 9 ++++++--- prizetap/validators.py | 2 +- tokenTap/validators.py | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index 3e967f8..20c9c46 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -56,6 +56,8 @@ class ConstraintVerification(ABC): app_name = ConstraintApp.GENERAL.value __response_text = "" is_cachable = True + invalid_cache_until = 60 + valid_cache_until = 60 * 60 def __init__(self, user_profile) -> None: self.user_profile = user_profile diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index f2b68aa..25daa2a 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -13,7 +13,9 @@ class HasVerifiedCloudflareCaptcha(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.GENERAL.value - is_cachable = False + is_cachable = True + valid_cache_until = 2 * 60 + invalid_cache_until = 0 def is_observed(self, *args, **kwargs) -> bool: diff --git a/core/utils.py b/core/utils.py index 873ac87..a834836 100644 --- a/core/utils.py +++ b/core/utils.py @@ -399,13 +399,16 @@ def get_client_ip(x_forwarded_for): -def cache_constraint_result(cache_key, is_verified, info): - caching_time = 60 * 60 if is_verified else 60 +def cache_constraint_result(cache_key, constraint, info): + caching_time = constraint.valid_cache_until if constraint.is_verified else constraint.invalid_cache_until expiration_time = time.time() + caching_time cache_data = { - "is_verified": is_verified, + "is_verified": constraint.is_verified, "info": info, "expiration_time": expiration_time, } + if caching_time <= 0: + return cache_data + cache.set(cache_key, cache_data, caching_time) return cache_data \ No newline at end of file diff --git a/prizetap/validators.py b/prizetap/validators.py index 9dc8abe..b8436e8 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -61,7 +61,7 @@ def check_user_constraints(self, raise_exception=True): ) if constraint.is_cachable: - constraint_data = cache_constraint_result(cache_key, is_verified, info) + constraint_data = cache_constraint_result(cache_key, constraint, info) else: constraint_data = {"is_verified": is_verified, "info": info} diff --git a/tokenTap/validators.py b/tokenTap/validators.py index a33698e..601a945 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -93,7 +93,7 @@ def check_user_permissions(self, raise_exception=True): ) if constraint.is_cachable: - constraint_data = cache_constraint_result(cache_key, is_verified, info) + constraint_data = cache_constraint_result(cache_key, constraint, info) else: constraint_data = {"is_verified": is_verified, "info": info} From 6157016edd5aad07bd4bbbab2b853074c7f841bf Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 26 Aug 2024 22:54:30 +0330 Subject: [PATCH 045/110] fixed errors --- core/utils.py | 6 +++--- prizetap/validators.py | 2 +- tokenTap/validators.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/utils.py b/core/utils.py index a834836..47d5175 100644 --- a/core/utils.py +++ b/core/utils.py @@ -399,11 +399,11 @@ def get_client_ip(x_forwarded_for): -def cache_constraint_result(cache_key, constraint, info): - caching_time = constraint.valid_cache_until if constraint.is_verified else constraint.invalid_cache_until +def cache_constraint_result(cache_key, is_verified, constraint, info): + caching_time = constraint.valid_cache_until if is_verified else constraint.invalid_cache_until expiration_time = time.time() + caching_time cache_data = { - "is_verified": constraint.is_verified, + "is_verified": is_verified, "info": info, "expiration_time": expiration_time, } diff --git a/prizetap/validators.py b/prizetap/validators.py index b8436e8..1f0ac1b 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -61,7 +61,7 @@ def check_user_constraints(self, raise_exception=True): ) if constraint.is_cachable: - constraint_data = cache_constraint_result(cache_key, constraint, info) + constraint_data = cache_constraint_result(cache_key, is_verified, constraint, info) else: constraint_data = {"is_verified": is_verified, "info": info} diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 601a945..f9682fc 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -93,7 +93,7 @@ def check_user_permissions(self, raise_exception=True): ) if constraint.is_cachable: - constraint_data = cache_constraint_result(cache_key, constraint, info) + constraint_data = cache_constraint_result(cache_key, is_verified, constraint, info) else: constraint_data = {"is_verified": is_verified, "info": info} From 30bd27561a76c8feff2ddfe795a3f89c4f386aa1 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Tue, 27 Aug 2024 13:16:12 +0330 Subject: [PATCH 046/110] fixed testing image column name --- tokenTap/tests.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tokenTap/tests.py b/tokenTap/tests.py index b3fb86d..95efdda 100644 --- a/tokenTap/tests.py +++ b/tokenTap/tests.py @@ -57,7 +57,7 @@ def test_token_distribution_creation(self): distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0x123456789abcdef", amount=1000, @@ -83,7 +83,7 @@ def test_token_distribution_expiration(self): distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0x123456789abcdef", amount=1000, @@ -100,7 +100,7 @@ def test_token_distribution_expiration(self): distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0x123456789abcdef", amount=1000, @@ -149,7 +149,7 @@ def setUp(self) -> None: distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0xd78Bc9369ef4617F5E3965d47838a0FCc4B9145F", amount=1000, @@ -287,7 +287,7 @@ def setUp(self) -> None: distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0x83ff60e2f93f8edd0637ef669c69d5fb4f64ca8e", amount=1000, @@ -338,7 +338,7 @@ def test_token_distribution_not_claimable_max_reached(self): distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0x123456789abcdef", amount=1000, @@ -366,7 +366,7 @@ def test_token_distribution_not_claimable_deadline_reached(self): distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0x123456789abcdef", amount=1000, @@ -520,7 +520,7 @@ def setUp(self) -> None: distributor_url="https://example.com/distributor", discord_url="https://discord.com/example", twitter_url="https://twitter.com/example", - image_url="https://example.com/image.png", + image="https://example.com/image.png", token="TEST", token_address="0x83ff60e2f93f8edd0637ef669c69d5fb4f64ca8e", amount=1000, From 3a8cf165769e882089bc9fe6e4c5783e0d51fabb Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Tue, 27 Aug 2024 13:27:49 +0330 Subject: [PATCH 047/110] fixed migration issue --- .../migrations/0077_remove_raffle_image_url_raffle_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py b/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py index 4843694..069d34f 100644 --- a/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py +++ b/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py @@ -18,7 +18,7 @@ class Migration(migrations.Migration): ), migrations.AlterField( model_name='raffle', - name='image_url', + name='image', field=cloudflare_images.field.CloudflareImagesField(blank=True, max_length=255, null=True, upload_to='', variant='public'), ), ] \ No newline at end of file From 787b1475a96046a8b7a47fd8ae355edcce191766 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Wed, 28 Aug 2024 20:22:45 +0330 Subject: [PATCH 048/110] Add zora constraint --- core/constraints/__init__.py | 3 +- core/constraints/abstract.py | 4 ++- core/constraints/arbitrum.py | 2 +- core/constraints/zora.py | 33 +++++++++++++++++ core/models.py | 5 ++- core/thirdpartyapp/__init__.py | 1 + core/thirdpartyapp/config.py | 1 + core/thirdpartyapp/subgraph.py | 4 +-- core/thirdpartyapp/zora.py | 36 +++++++++++++++++++ .../migrations/0077_alter_constraint_name.py | 29 +++++++++++++++ prizetap/validators.py | 11 +++--- .../migrations/0063_alter_constraint_name.py | 28 +++++++++++++++ tokenTap/validators.py | 16 ++++----- 13 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 core/constraints/zora.py create mode 100644 core/thirdpartyapp/zora.py create mode 100644 prizetap/migrations/0077_alter_constraint_name.py create mode 100644 tokenTap/migrations/0063_alter_constraint_name.py diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index 648d948..9f6c180 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -16,6 +16,7 @@ BrightIDAuraVerification, BrightIDMeetVerification, ) +from core.constraints.captcha import HasVerifiedCloudflareCaptcha from core.constraints.EAS import Attest, BeAttestedBy from core.constraints.ens import HasENSVerification from core.constraints.farcaster import ( @@ -63,7 +64,7 @@ IsFollowingTwitterBatch, IsFollowinTwitterUser, ) -from core.constraints.captcha import HasVerifiedCloudflareCaptcha +from core.constraints.zora import DidMintZoraNFT def get_constraint(constraint_label: str) -> ConstraintVerification: diff --git a/core/constraints/abstract.py b/core/constraints/abstract.py index 20c9c46..8418d84 100644 --- a/core/constraints/abstract.py +++ b/core/constraints/abstract.py @@ -16,6 +16,7 @@ class ConstraintApp(Enum): MUON = "muon" OPTIMISM = "optimism" OCTANT = "octant" + ZORA = "zora" @classmethod def choices(cls): @@ -59,9 +60,10 @@ class ConstraintVerification(ABC): invalid_cache_until = 60 valid_cache_until = 60 * 60 - def __init__(self, user_profile) -> None: + def __init__(self, user_profile, *, obj=None) -> None: self.user_profile = user_profile self._param_values = {} + self.obj = obj def get_info(self, *args, **kwargs): pass diff --git a/core/constraints/arbitrum.py b/core/constraints/arbitrum.py index 79d5587..bc80222 100644 --- a/core/constraints/arbitrum.py +++ b/core/constraints/arbitrum.py @@ -68,7 +68,7 @@ def has_bridged(self, from_time=None): } res = subgraph.send_post_request( - subgraph.path.get("arb_bridge_mainnet"), query=query, vars=vars + subgraph.paths.get("arb_bridge_mainnet"), query=query, vars=vars ) match res: case None: diff --git a/core/constraints/zora.py b/core/constraints/zora.py new file mode 100644 index 0000000..bed49bd --- /dev/null +++ b/core/constraints/zora.py @@ -0,0 +1,33 @@ +from datetime import datetime + +from core.constraints.abstract import ( + ConstraintApp, + ConstraintParam, + ConstraintVerification, +) +from core.thirdpartyapp import ZoraUtil + + +class DidMintZoraNFT(ConstraintVerification): + app_name = ConstraintApp.ZORA.value + _param_keys = [ConstraintParam.ADDRESS] + + def __init__(self, user_profile) -> None: + super().__init__(user_profile) + + def is_observed(self, *args, **kwargs) -> bool: + zora_util = ZoraUtil() + user_addresses = self.user_addresses + nft_address = self.param_values[ConstraintParam.ADDRESS.name] + for address in user_addresses: + res = zora_util.get_tx(nft_address=nft_address, address=address) + if res is None: + continue + for tx in res.values: + if ( + tx.get("method") == "mint" + and datetime.strptime(tx.get("timestamp"), "%Y-%m-%dT%H:%M:%S.%fZ") + > self.obj.start_at + ): + return True + return False diff --git a/core/models.py b/core/models.py index 547fbb5..1a2d1d6 100644 --- a/core/models.py +++ b/core/models.py @@ -6,12 +6,13 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils.translation import gettext_lazy as _ -from core.constraints.captcha import HasVerifiedCloudflareCaptcha from encrypted_model_fields.fields import EncryptedCharField from rest_framework.exceptions import ValidationError from solders.keypair import Keypair from solders.pubkey import Pubkey +from core.constraints.captcha import HasVerifiedCloudflareCaptcha + from .constraints import ( AllowListVerification, Attest, @@ -28,6 +29,7 @@ DidDelegateArbToAddress, DidDelegateOPToAddress, DidLikedFarcasterCast, + DidMintZoraNFT, DidMirrorOnLensPublication, DidQuoteTweet, DidRecastFarcasterCast, @@ -157,6 +159,7 @@ class Type(models.TextChoices): IsFollowingTwitterBatch, IsFollowingFarcasterBatch, HasVerifiedCloudflareCaptcha, + DidMintZoraNFT, ] name = models.CharField( diff --git a/core/thirdpartyapp/__init__.py b/core/thirdpartyapp/__init__.py index bd7c417..b05c399 100644 --- a/core/thirdpartyapp/__init__.py +++ b/core/thirdpartyapp/__init__.py @@ -5,3 +5,4 @@ from .lens import LensUtil # noqa: F401 from .subgraph import Subgraph from .twitter import RapidTwitter, TwitterUtils +from .zora import ZoraUtil diff --git a/core/thirdpartyapp/config.py b/core/thirdpartyapp/config.py index b3be82c..d65b330 100644 --- a/core/thirdpartyapp/config.py +++ b/core/thirdpartyapp/config.py @@ -22,3 +22,4 @@ ), } SUBGRAPH_BASE_URL = os.getenv("SUBGRAPH_BASE_URL", "https://api.studio.thegraph.com") +ZORA_BASE_URL = os.getenv("ZORA_BASE_URL", "https://explorer.zora.energy") diff --git a/core/thirdpartyapp/subgraph.py b/core/thirdpartyapp/subgraph.py index 64ed81c..7ad28a3 100644 --- a/core/thirdpartyapp/subgraph.py +++ b/core/thirdpartyapp/subgraph.py @@ -7,7 +7,7 @@ class Subgraph: requests = RequestHelper(config.SUBGRAPH_BASE_URL) - path = { + paths = { "unitap_pass": "query/73675/unitap-pass-eth/version/latest", "arb_bridge_mainnet": "query/21879/unitap-arb-bridge-mainnet/version/latest", } @@ -53,7 +53,7 @@ def get_unitap_pass_holders( while True: vars["skip"] = count res = self.send_post_request( - self.path.get("unitap_pass"), query=query, vars=vars + self.paths.get("unitap_pass"), query=query, vars=vars ) match res: case None: diff --git a/core/thirdpartyapp/zora.py b/core/thirdpartyapp/zora.py new file mode 100644 index 0000000..2472975 --- /dev/null +++ b/core/thirdpartyapp/zora.py @@ -0,0 +1,36 @@ +import logging + +from core.request_helper import RequestException, RequestHelper +from core.thirdpartyapp import config + + +class ZoraUtil: + request = RequestHelper(base_url=config.ZORA_BASE_URL) + paths = {"get-address-token-transfer": "api/v2/addresses/{address}/token-transfers"} + + def __init__(self) -> None: + self.session = self.request.get_session() + + @property + def headers(self): + return {"accept: application/json"} + + def get_tx(self, nft_address: str, address: str): + params = { + "type": "ERC-20,ERC-721,ERC-1155", + "filter": "to", + "token": nft_address, + } + try: + res = self.request.get( + path=self.paths.get("get-address-token-transfer").format( + address=address + ), + session=self.session, + headers=self.headers, + params=params, + ) + return res.json() + except RequestException as e: + logging.error(f"Could not get token info from zora: {str(e)}") + return None diff --git a/prizetap/migrations/0077_alter_constraint_name.py b/prizetap/migrations/0077_alter_constraint_name.py new file mode 100644 index 0000000..641adea --- /dev/null +++ b/prizetap/migrations/0077_alter_constraint_name.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.4 on 2024-08-28 16:19 + +from django.db import migrations, models + +def create_prizetap_constraint(apps, schema_editor): + Constraint = apps.get_model("prizetap", "Constraint") + + Constraint.objects.create( + name="core.DidMintZoraNFT", + description="DidMintZoraNFT", + title="Did Mint Zora NFT", + type="VER", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0076_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('core.DidMintZoraNFT', 'DidMintZoraNFT'), ('prizetap.HaveUnitapPass', 'HaveUnitapPass'), ('prizetap.NotHaveUnitapPass', 'NotHaveUnitapPass'), ('faucet.OptimismDonationConstraint', 'OptimismDonationConstraint'), ('faucet.OptimismClaimingGasConstraint', 'OptimismClaimingGasConstraint')], max_length=255, unique=True), + ), + migrations.RunPython(create_prizetap_constraint), + ] diff --git a/prizetap/validators.py b/prizetap/validators.py index 1f0ac1b..1cdb9b4 100644 --- a/prizetap/validators.py +++ b/prizetap/validators.py @@ -1,5 +1,4 @@ import json -import time from django.core.cache import cache from rest_framework.exceptions import PermissionDenied, ValidationError @@ -31,7 +30,7 @@ def check_user_constraints(self, raise_exception=True): result = dict() for c in self.raffle.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile + self.user_profile, obj=self.raffle ) constraint.response = c.response try: @@ -57,11 +56,15 @@ def check_user_constraints(self, raise_exception=True): ) else: is_verified = constraint.is_observed( - **cdata, from_time=int(self.raffle.start_at.timestamp()), context={"request": self.request} + **cdata, + from_time=int(self.raffle.start_at.timestamp()), + context={"request": self.request}, ) if constraint.is_cachable: - constraint_data = cache_constraint_result(cache_key, is_verified, constraint, info) + constraint_data = cache_constraint_result( + cache_key, is_verified, constraint, info + ) else: constraint_data = {"is_verified": is_verified, "info": info} diff --git a/tokenTap/migrations/0063_alter_constraint_name.py b/tokenTap/migrations/0063_alter_constraint_name.py new file mode 100644 index 0000000..821c7d5 --- /dev/null +++ b/tokenTap/migrations/0063_alter_constraint_name.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.4 on 2024-08-28 16:19 + +from django.db import migrations, models + +def create_tokentap_constraint(apps, schema_editor): + Constraint = apps.get_model("tokenTap", "Constraint") + + Constraint.objects.create( + name="core.DidMintZoraNFT", + description="DidMintZoraNFT", + title="Did Mint Zora NFT", + type="VER", + ) + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0062_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('core.DidMintZoraNFT', 'DidMintZoraNFT'), ('tokenTap.OncePerMonthVerification', 'OncePerMonthVerification'), ('tokenTap.OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('faucet.OptimismHasClaimedGasConstraint', 'OptimismHasClaimedGasConstraint')], max_length=255, unique=True), + ), + migrations.RunPython(create_tokentap_constraint), + ] diff --git a/tokenTap/validators.py b/tokenTap/validators.py index f9682fc..8870880 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -12,8 +12,6 @@ from .models import ClaimReceipt, TokenDistribution - - class SetDistributionTxValidator: def __init__(self, *args, **kwargs): self.user_profile: UserProfile = kwargs["user_profile"] @@ -38,8 +36,6 @@ def is_valid(self, data): raise PermissionDenied("Tx hash is not valid") - - class TokenDistributionValidator: def __init__( self, @@ -64,14 +60,14 @@ def check_user_permissions(self, raise_exception=True): result = dict() for c in self.td.constraints.all(): constraint: ConstraintVerification = get_constraint(c.name)( - self.user_profile, + self.user_profile, obj=self.td ) constraint.response = c.response try: constraint.param_values = param_values[c.name] except KeyError: pass - + cdata = self.td_data.get(str(c.pk), dict()) cache_key = f"tokentap-{self.user_profile.pk}-{self.td.pk}-{c.pk}" constraint_data = cache.get(cache_key) @@ -89,11 +85,13 @@ def check_user_permissions(self, raise_exception=True): is_verified = constraint.is_observed( **cdata, token_distribution=self.td, - context={"request": self.request} + context={"request": self.request}, ) if constraint.is_cachable: - constraint_data = cache_constraint_result(cache_key, is_verified, constraint, info) + constraint_data = cache_constraint_result( + cache_key, is_verified, constraint, info + ) else: constraint_data = {"is_verified": is_verified, "info": info} @@ -103,7 +101,7 @@ def check_user_permissions(self, raise_exception=True): if len(error_messages) and raise_exception: raise PermissionDenied(error_messages) return result - + def cache_constraint(self): pass From 8335a1e1cbf93e9737e6fa48570c8285b9a51e12 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Fri, 30 Aug 2024 19:18:12 +0330 Subject: [PATCH 049/110] Add twitter_id filed to TwitterConnection --- .../0042_twitterconnection_twitter_id.py | 18 ++++++++++++++++++ authentication/models.py | 6 +++--- authentication/views.py | 12 ++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 authentication/migrations/0042_twitterconnection_twitter_id.py diff --git a/authentication/migrations/0042_twitterconnection_twitter_id.py b/authentication/migrations/0042_twitterconnection_twitter_id.py new file mode 100644 index 0000000..b908ca5 --- /dev/null +++ b/authentication/migrations/0042_twitterconnection_twitter_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2024-08-30 15:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0041_alter_brightidconnection_user_profile_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='twitterconnection', + name='twitter_id', + field=models.CharField(max_length=255, null=True, unique=True), + ), + ] diff --git a/authentication/models.py b/authentication/models.py index 4d89b29..51b2f08 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -260,10 +260,11 @@ class TwitterConnection(BaseThirdPartyConnection): access_token_secret = models.CharField( max_length=255, unique=True, blank=True, null=True ) + twitter_id = models.CharField(max_length=255, unique=True, null=True) driver = TwitterDriver() def is_connected(self): - return bool(self.access_token and self.access_token_secret) + return bool(self.twitter_id) @property def tweet_count(self): @@ -279,8 +280,7 @@ def follower_count(self): def username(self): return self.driver.get_username(self.access_token, self.access_token_secret) - @property - def twitter_id(self): + def get_twitter_id(self): return self.driver.get_twitter_id( self, self.access_token, self.access_token_secret ) diff --git a/authentication/views.py b/authentication/views.py index 647bb1e..48c408b 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -738,7 +738,15 @@ def get(self, request, *args, **kwargs): twitter_connection.access_token = access_token twitter_connection.access_token_secret = access_token_secret + twitter_connection.twitter_id = twitter_connection.get_twitter_id() - twitter_connection.save(update_fields=("access_token", "access_token_secret")) - + try: + twitter_connection.save( + update_fields=("access_token", "access_token_secret", "twitter_id") + ) + except IntegrityError: + raise ValidationError( + """We can not connect you twitter account, + may be your account is connected before""" + ) return Response({}, HTTP_200_OK) From 41b8f20e541f928635c15dafa6b8151499f37ec4 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Fri, 30 Aug 2024 19:32:43 +0330 Subject: [PATCH 050/110] Fix test in twitter constraint --- core/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/tests.py b/core/tests.py index 300b8bb..79c9447 100644 --- a/core/tests.py +++ b/core/tests.py @@ -396,6 +396,7 @@ def setUp(self): oauth_token_secret=oauth_token_secret, access_token="test", access_token_secret="test", + twitter_id="1", ) self.not_connected_user_profile = UserProfile.objects.create( From 9fca8703436c7770258009f23a48118cbc9bacb8 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Fri, 30 Aug 2024 19:41:42 +0330 Subject: [PATCH 051/110] Fix get_twitter_id --- authentication/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/authentication/models.py b/authentication/models.py index 51b2f08..0d1f5af 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -281,9 +281,7 @@ def username(self): return self.driver.get_username(self.access_token, self.access_token_secret) def get_twitter_id(self): - return self.driver.get_twitter_id( - self, self.access_token, self.access_token_secret - ) + return self.driver.get_twitter_id(self.access_token, self.access_token_secret) def is_replied(self, self_tweet_id, target_tweet_id): return self.driver.get_is_replied( From 0da60d80901d2933ae70968db04725e61057fe8c Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 1 Sep 2024 15:20:29 +0330 Subject: [PATCH 052/110] added hcaptcha setup --- brightIDfaucet/settings.py | 1 + core/constraints/__init__.py | 2 +- core/constraints/captcha.py | 30 +++++++++++++++++ core/models.py | 3 +- core/thirdpartyapp/hcaptcha.py | 24 ++++++++++++++ .../migrations/0078_alter_constraint_name.py | 32 +++++++++++++++++++ .../migrations/0064_alter_constraint_name.py | 31 ++++++++++++++++++ 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 core/thirdpartyapp/hcaptcha.py create mode 100644 prizetap/migrations/0078_alter_constraint_name.py create mode 100644 tokenTap/migrations/0064_alter_constraint_name.py diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 5a18aee..702c208 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -73,6 +73,7 @@ def str2bool(v): DEPLOYMENT_ENV = os.environ.get("DEPLOYMENT_ENV") CLOUDFLARE_TURNSTILE_SECRET_KEY = os.environ.get("CLOUDFLARE_TURNSTILE_SECRET_KEY") +H_CAPTCHA_SECRET = os.environ.get("H_CAPTCHA_SECRET") assert DEPLOYMENT_ENV in ["dev", "main"] diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index 9f6c180..74decfe 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -16,7 +16,7 @@ BrightIDAuraVerification, BrightIDMeetVerification, ) -from core.constraints.captcha import HasVerifiedCloudflareCaptcha +from core.constraints.captcha import HasVerifiedCloudflareCaptcha, HasVerifiedHCaptcha from core.constraints.EAS import Attest, BeAttestedBy from core.constraints.ens import HasENSVerification from core.constraints.farcaster import ( diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index 25daa2a..070ef11 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -4,6 +4,7 @@ import logging +from core.thirdpartyapp.hcaptcha import HCaptchaUtil from core.utils import RequestContextExtractor @@ -35,3 +36,32 @@ def is_observed(self, *args, **kwargs) -> bool: return request_context.ip is not None and turnstile_token is not None and cloudflare.is_verified( turnstile_token, request_context.ip ) + + + + +class HasVerifiedHCaptcha(ConstraintVerification): + _param_keys = [] + app_name = ConstraintApp.GENERAL.value + is_cachable = True + valid_cache_until = 2 * 60 + invalid_cache_until = 0 + + def is_observed(self, *args, **kwargs) -> bool: + + context = kwargs.get("context") + + if context is None or context.get("request") is None: + return False + + hcaptcha = HCaptchaUtil() + + request_context: RequestContextExtractor = RequestContextExtractor( + context["request"] + ) + + turnstile_token = request_context.data.get("cf-turnstile-response") + + return request_context.ip is not None and turnstile_token is not None and hcaptcha.is_verified( + turnstile_token, request_context.ip + ) \ No newline at end of file diff --git a/core/models.py b/core/models.py index 1a2d1d6..dea31cb 100644 --- a/core/models.py +++ b/core/models.py @@ -11,7 +11,7 @@ from solders.keypair import Keypair from solders.pubkey import Pubkey -from core.constraints.captcha import HasVerifiedCloudflareCaptcha +from core.constraints.captcha import HasVerifiedCloudflareCaptcha, HasVerifiedHCaptcha from .constraints import ( AllowListVerification, @@ -160,6 +160,7 @@ class Type(models.TextChoices): IsFollowingFarcasterBatch, HasVerifiedCloudflareCaptcha, DidMintZoraNFT, + HasVerifiedHCaptcha ] name = models.CharField( diff --git a/core/thirdpartyapp/hcaptcha.py b/core/thirdpartyapp/hcaptcha.py new file mode 100644 index 0000000..3090f2f --- /dev/null +++ b/core/thirdpartyapp/hcaptcha.py @@ -0,0 +1,24 @@ +import logging +from django.conf import settings +import requests + + +logger = logging.getLogger(__name__) + + +class HCaptchaUtil: + secret_key = settings.H_CAPTCHA_SECRET + api_url = "https://api.hcaptcha.com" + + def is_verified(self, token: str, ip: str) -> bool: + try: + res = requests.post( + f"{self.api_url}/siteverify", + data={"secret": self.secret_key, "response": token, "remoteip": ip}, + ) + + return res.ok and res.json()["success"] + except Exception as e: + logger.info(f"Error occurred during hcaptcha verification {str(e)}") + + return False diff --git a/prizetap/migrations/0078_alter_constraint_name.py b/prizetap/migrations/0078_alter_constraint_name.py new file mode 100644 index 0000000..d66078c --- /dev/null +++ b/prizetap/migrations/0078_alter_constraint_name.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0 on 2024-09-01 11:40 + +from django.db import migrations, models + + + +def create_prizetap_constraint(apps, schema_editor): + Constraint = apps.get_model("prizetap", "Constraint") + + Constraint.objects.create( + name="core.HasVerifiedHCaptcha", + description="HasVerifiedHCaptcha", + title="Passed HCaptcha Captcha", + type="VER", + ) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0077_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('core.DidMintZoraNFT', 'DidMintZoraNFT'), ('core.HasVerifiedHCaptcha', 'HasVerifiedHCaptcha'), ('prizetap.HaveUnitapPass', 'HaveUnitapPass'), ('prizetap.NotHaveUnitapPass', 'NotHaveUnitapPass'), ('faucet.OptimismDonationConstraint', 'OptimismDonationConstraint'), ('faucet.OptimismClaimingGasConstraint', 'OptimismClaimingGasConstraint')], max_length=255, unique=True), + ), + migrations.RunPython(create_prizetap_constraint) + ] diff --git a/tokenTap/migrations/0064_alter_constraint_name.py b/tokenTap/migrations/0064_alter_constraint_name.py new file mode 100644 index 0000000..947c3dd --- /dev/null +++ b/tokenTap/migrations/0064_alter_constraint_name.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0 on 2024-09-01 11:40 + +from django.db import migrations, models + + +def create_tokentap_constraint(apps, schema_editor): + Constraint = apps.get_model("tokenTap", "Constraint") + + Constraint.objects.create( + name="core.HasVerifiedHCaptcha", + description="HasVerifiedHCaptcha", + title="Passed HCaptcha Captcha", + type="VER", + ) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0063_alter_constraint_name'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowinTwitterUser', 'IsFollowinTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('core.DidMintZoraNFT', 'DidMintZoraNFT'), ('core.HasVerifiedHCaptcha', 'HasVerifiedHCaptcha'), ('tokenTap.OncePerMonthVerification', 'OncePerMonthVerification'), ('tokenTap.OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('faucet.OptimismHasClaimedGasConstraint', 'OptimismHasClaimedGasConstraint')], max_length=255, unique=True), + ), + migrations.RunPython(create_tokentap_constraint) + ] From acde1fdcc900c40bcf30ec184a8c0af07297209f Mon Sep 17 00:00:00 2001 From: Shayan Shiravani Date: Tue, 3 Sep 2024 14:49:05 +0330 Subject: [PATCH 053/110] requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 924296c..a1ba658 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,4 +33,5 @@ django-safedelete~=1.3.3 tweepy~=4.14.0 ratelimit~=2.2.1 pillow==10.4.0 -django-cloudflare-images~=0.6.0 \ No newline at end of file +django-cloudflare-images~=0.6.0 +zstandard~=0.15.0 From ed68b4ae6776252ff77176c2bb357313a1a9108a Mon Sep 17 00:00:00 2001 From: Shayan Shiravani Date: Tue, 3 Sep 2024 15:02:15 +0330 Subject: [PATCH 054/110] bugfix --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a1ba658..958b6a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,4 +34,4 @@ tweepy~=4.14.0 ratelimit~=2.2.1 pillow==10.4.0 django-cloudflare-images~=0.6.0 -zstandard~=0.15.0 +zstandard~=0.17.0 From b89e3235c31bdc7ef91d59cf0d3ba67b496ca353 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Tue, 3 Sep 2024 16:19:26 +0330 Subject: [PATCH 055/110] fixed migrations --- .../migrations/0077_remove_raffle_image_url_raffle_image.py | 2 +- prizetap/migrations/0078_alter_constraint_name.py | 2 +- ...063_remove_tokendistribution_token_image_url_and_more.py | 6 +++--- tokenTap/migrations/0064_alter_constraint_name.py | 2 +- tokenTap/models.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py b/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py index 069d34f..b6d483d 100644 --- a/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py +++ b/prizetap/migrations/0077_remove_raffle_image_url_raffle_image.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('prizetap', '0076_alter_constraint_name'), + ('prizetap', '0077_alter_constraint_name'), ] operations = [ diff --git a/prizetap/migrations/0078_alter_constraint_name.py b/prizetap/migrations/0078_alter_constraint_name.py index d66078c..7c62f9c 100644 --- a/prizetap/migrations/0078_alter_constraint_name.py +++ b/prizetap/migrations/0078_alter_constraint_name.py @@ -19,7 +19,7 @@ def create_prizetap_constraint(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('prizetap', '0077_alter_constraint_name'), + ('prizetap', '0077_remove_raffle_image_url_raffle_image'), ] operations = [ diff --git a/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py b/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py index 4cac9b8..ca72f5a 100644 --- a/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py +++ b/tokenTap/migrations/0063_remove_tokendistribution_token_image_url_and_more.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('tokenTap', '0062_alter_constraint_name'), + ('tokenTap', '0063_alter_constraint_name'), ] operations = [ @@ -19,7 +19,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='tokendistribution', name='token_image', - field=cloudflare_images.field.CloudflareImagesField(blank=True, upload_to='', variant='public'), + field=cloudflare_images.field.CloudflareImagesField(blank=True, null=True, upload_to='', variant='public'), ), migrations.RenameField( model_name='tokendistribution', @@ -29,6 +29,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='tokendistribution', name='image', - field=cloudflare_images.field.CloudflareImagesField(blank=True, upload_to='', variant='public'), + field=cloudflare_images.field.CloudflareImagesField(blank=True, null=True, upload_to='', variant='public'), ), ] diff --git a/tokenTap/migrations/0064_alter_constraint_name.py b/tokenTap/migrations/0064_alter_constraint_name.py index 947c3dd..f87ff76 100644 --- a/tokenTap/migrations/0064_alter_constraint_name.py +++ b/tokenTap/migrations/0064_alter_constraint_name.py @@ -18,7 +18,7 @@ def create_tokentap_constraint(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('tokenTap', '0063_alter_constraint_name'), + ('tokenTap', '0063_remove_tokendistribution_token_image_url_and_more'), ] operations = [ diff --git a/tokenTap/models.py b/tokenTap/models.py index f072619..d5e9b1c 100644 --- a/tokenTap/models.py +++ b/tokenTap/models.py @@ -49,8 +49,8 @@ class Status(models.TextChoices): twitter_url = models.URLField(max_length=255, null=True, blank=True) email_url = models.EmailField(max_length=255) telegram_url = models.URLField(max_length=255, null=True, blank=True) - image = CloudflareImagesField(variant="public", blank=True) - token_image = CloudflareImagesField(variant="public", blank=True) + image = CloudflareImagesField(variant="public", null=True, blank=True) + token_image = CloudflareImagesField(variant="public", null=True, blank=True) token = models.CharField(max_length=100) token_address = models.CharField(max_length=255) From 0004e2e68278b54cc50f976a7ebf2d1494bbe12e Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 7 Sep 2024 12:37:47 +0330 Subject: [PATCH 056/110] fixed zora syntax error --- core/constraints/zora.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/constraints/zora.py b/core/constraints/zora.py index bed49bd..6d791d0 100644 --- a/core/constraints/zora.py +++ b/core/constraints/zora.py @@ -12,8 +12,8 @@ class DidMintZoraNFT(ConstraintVerification): app_name = ConstraintApp.ZORA.value _param_keys = [ConstraintParam.ADDRESS] - def __init__(self, user_profile) -> None: - super().__init__(user_profile) + def __init__(self, user_profile, *, obj=None) -> None: + super().__init__(user_profile, obj=obj) def is_observed(self, *args, **kwargs) -> bool: zora_util = ZoraUtil() From 2b375da7d003e6f8e2102759a50dcf7384dcbc69 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 7 Sep 2024 12:38:34 +0330 Subject: [PATCH 057/110] modified turnsite token key for hcaptcha --- core/constraints/captcha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index 070ef11..6b83d38 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -60,7 +60,7 @@ def is_observed(self, *args, **kwargs) -> bool: context["request"] ) - turnstile_token = request_context.data.get("cf-turnstile-response") + turnstile_token = request_context.data.get("hc-turnstile-response") or request_context.data.get("cf-turnstile-response") return request_context.ip is not None and turnstile_token is not None and hcaptcha.is_verified( turnstile_token, request_context.ip From 1c3f4fdc65f9101e5fe730708b8230745f29be47 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 7 Sep 2024 13:05:10 +0330 Subject: [PATCH 058/110] modified reading tokens from headers --- brightIDfaucet/settings.py | 7 +++++++ core/constraints/captcha.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 79dbf66..bbedd01 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -7,6 +7,8 @@ from dotenv import load_dotenv from sentry_sdk.integrations.django import DjangoIntegration +from corsheaders.defaults import default_headers + from faucet.faucet_manager.bright_id_interface import BrightIDInterface load_dotenv() @@ -244,6 +246,11 @@ def before_send(event, hint): else: CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_HEADERS = list(default_headers) + [ + 'cf-turnstile-response', + 'hc-turnstile-response', +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ diff --git a/core/constraints/captcha.py b/core/constraints/captcha.py index 6b83d38..7c29e3a 100644 --- a/core/constraints/captcha.py +++ b/core/constraints/captcha.py @@ -31,7 +31,7 @@ def is_observed(self, *args, **kwargs) -> bool: context["request"] ) - turnstile_token = request_context.data.get("cf-turnstile-response") + turnstile_token = request_context.headers.get("cf-turnstile-response") return request_context.ip is not None and turnstile_token is not None and cloudflare.is_verified( turnstile_token, request_context.ip @@ -60,7 +60,7 @@ def is_observed(self, *args, **kwargs) -> bool: context["request"] ) - turnstile_token = request_context.data.get("hc-turnstile-response") or request_context.data.get("cf-turnstile-response") + turnstile_token = request_context.headers.get("hc-turnstile-response") return request_context.ip is not None and turnstile_token is not None and hcaptcha.is_verified( turnstile_token, request_context.ip From 67b63e6409d034a202252178bf342d3457631b6a Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 7 Sep 2024 13:09:37 +0330 Subject: [PATCH 059/110] added comments for more readability --- brightIDfaucet/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index bbedd01..bafc6cb 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -246,6 +246,10 @@ def before_send(event, hint): else: CORS_ALLOW_ALL_ORIGINS = True + +# Add Turnstile response headers for CORS +# These headers are required for Cloudflare and HCaptcha Turnstile anti-bot service + CORS_ALLOW_HEADERS = list(default_headers) + [ 'cf-turnstile-response', 'hc-turnstile-response', From 3e7f12488b38886e386da198a79d92f306ded6ad Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Fri, 13 Sep 2024 12:20:03 +0330 Subject: [PATCH 060/110] fixed record urls --- .../migrations/0079_fix_raffle_image_urls.py | 32 +++++++++++++++ .../migrations/0065_fix_token_image_prefix.py | 40 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 prizetap/migrations/0079_fix_raffle_image_urls.py create mode 100644 tokenTap/migrations/0065_fix_token_image_prefix.py diff --git a/prizetap/migrations/0079_fix_raffle_image_urls.py b/prizetap/migrations/0079_fix_raffle_image_urls.py new file mode 100644 index 0000000..7574063 --- /dev/null +++ b/prizetap/migrations/0079_fix_raffle_image_urls.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.4 on 2024-08-25 09:12 + +from django.db import migrations, models + + +def fix_raffle_images_prefix(apps, schema): + Raffle = apps.get_model("prizetap", "Raffle") + + raffles = Raffle.objects.all() + + for raffle in raffles: + if ( + raffle.image + and raffle.image.name + and raffle.image.name.startswith("https://imagedelivery.net") + ): + # split the url to get the image id + raffle.image.name = raffle.image.name.split("/")[-2] + raffle.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("tokenTap", "0078_alter_constraint_name"), + ] + + operations = [ + migrations.RunPython( + fix_raffle_images_prefix, reverse_code=migrations.RunPython.noop + ) + ] diff --git a/tokenTap/migrations/0065_fix_token_image_prefix.py b/tokenTap/migrations/0065_fix_token_image_prefix.py new file mode 100644 index 0000000..d1df6a4 --- /dev/null +++ b/tokenTap/migrations/0065_fix_token_image_prefix.py @@ -0,0 +1,40 @@ +# Generated by Django 4.0.4 on 2024-08-25 09:12 + +from django.db import migrations, models + + +def fix_token_images_prefix(apps, schema_editor): + TokenDistribution = apps.get_model("tokenTap", "TokenDistribution") + + tokens = TokenDistribution.objects.all() + + for token in tokens: + if ( + token.image + and token.image.name + and token.image.name.startswith("https://imagedelivery.net") + ): + # split the url to get the image id + token.image.name = token.image.name.split("/")[-2] + token.save() + + if ( + token.token_image + and token.token_image.name + and token.token_image.name.startswith("https://imagedelivery.net") + ): + token.token_image.name = token.token_image.name.split("/")[-2] + token.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("tokenTap", "0064_alter_constraint_name"), + ] + + operations = [ + migrations.RunPython( + fix_token_images_prefix, reverse_code=migrations.RunPython.noop + ) + ] From a1914237c69ee61d744452ac89fa57fee404c270 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Fri, 13 Sep 2024 12:23:46 +0330 Subject: [PATCH 061/110] fixed parent node migration issue --- prizetap/migrations/0079_fix_raffle_image_urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prizetap/migrations/0079_fix_raffle_image_urls.py b/prizetap/migrations/0079_fix_raffle_image_urls.py index 7574063..cf85d93 100644 --- a/prizetap/migrations/0079_fix_raffle_image_urls.py +++ b/prizetap/migrations/0079_fix_raffle_image_urls.py @@ -22,7 +22,7 @@ def fix_raffle_images_prefix(apps, schema): class Migration(migrations.Migration): dependencies = [ - ("tokenTap", "0078_alter_constraint_name"), + ("prizetap", "0078_alter_constraint_name"), ] operations = [ From 9435209db42cdc23d4238d57dadd108e93262a54 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 16 Sep 2024 20:56:39 +0330 Subject: [PATCH 062/110] fixed arguments issue --- core/constraints/arbitrum.py | 9 --------- core/constraints/ens.py | 3 --- core/constraints/farcaster.py | 21 --------------------- core/constraints/general.py | 12 ------------ core/constraints/gitcoin_passport.py | 9 --------- core/constraints/lens.py | 21 --------------------- core/constraints/muon_node.py | 4 ++-- core/constraints/octant.py | 3 --- core/constraints/optimism.py | 6 ------ core/constraints/twitter.py | 17 +---------------- 10 files changed, 3 insertions(+), 102 deletions(-) diff --git a/core/constraints/arbitrum.py b/core/constraints/arbitrum.py index bc80222..c9c1714 100644 --- a/core/constraints/arbitrum.py +++ b/core/constraints/arbitrum.py @@ -14,9 +14,6 @@ class BridgeEthToArb(ConstraintVerification): app_name = ConstraintApp.ARBITRUM.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: try: return self.has_bridged( @@ -107,9 +104,6 @@ class DelegateArb(ConstraintVerification): ] ARB_TOKEN_CONTRACT = "0x912CE59144191C1204E64559FE8253a0e49E6548" - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from core.models import Chain @@ -146,6 +140,3 @@ class DidDelegateArbToAddress(DelegateArb): ConstraintParam.ADDRESS, ConstraintParam.MINIMUM, ) - - def __init__(self, user_profile) -> None: - super().__init__(user_profile) diff --git a/core/constraints/ens.py b/core/constraints/ens.py index cd16092..bb26c57 100644 --- a/core/constraints/ens.py +++ b/core/constraints/ens.py @@ -5,9 +5,6 @@ class HasENSVerification(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.ENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import ENSConnection diff --git a/core/constraints/farcaster.py b/core/constraints/farcaster.py index 0dd88eb..fe811fd 100644 --- a/core/constraints/farcaster.py +++ b/core/constraints/farcaster.py @@ -12,9 +12,6 @@ class HasFarcasterProfile(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.FARCASTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import FarcasterConnection @@ -30,9 +27,6 @@ class IsFollowingFarcasterUser(ConstraintVerification): _param_keys = [ConstraintParam.FARCASTER_FID] app_name = ConstraintApp.FARCASTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import FarcasterConnection @@ -74,9 +68,6 @@ class DidLikedFarcasterCast(ConstraintVerification): _param_keys = [ConstraintParam.FARCASTER_CAST_HASH] app_name = ConstraintApp.FARCASTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import FarcasterConnection @@ -98,9 +89,6 @@ class DidRecastFarcasterCast(ConstraintVerification): _param_keys = [ConstraintParam.FARCASTER_CAST_HASH] app_name = ConstraintApp.FARCASTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import FarcasterConnection @@ -122,9 +110,6 @@ class HasMinimumFarcasterFollower(ConstraintVerification): _param_keys = [ConstraintParam.MINIMUM] app_name = ConstraintApp.FARCASTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import FarcasterConnection @@ -148,9 +133,6 @@ class IsFollowingFarcasterChannel(ConstraintVerification): _param_keys = [ConstraintParam.FARCASTER_CHANNEL_ID] app_name = ConstraintApp.FARCASTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import FarcasterConnection @@ -173,9 +155,6 @@ class IsFollowingFarcasterBatch(ConstraintVerification): _param_keys = [ConstraintParam.FARCASTER_FIDS] app_name = ConstraintApp.FARCASTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def get_info(self, *args, **kwargs) -> dict: from authentication.models import FarcasterConnection diff --git a/core/constraints/general.py b/core/constraints/general.py index 959c22f..249c07f 100644 --- a/core/constraints/general.py +++ b/core/constraints/general.py @@ -15,9 +15,6 @@ class HasNFTVerification(ConstraintVerification): ConstraintParam.MINIMUM, ] - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs): from core.models import Chain @@ -49,9 +46,6 @@ class ABCTokenVerification(ConstraintVerification, ABC): ConstraintParam.MINIMUM, ] - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - @abstractmethod def get_amount( self, user_address: str, token_address: str, token_client: TokenClient @@ -88,9 +82,6 @@ def is_observed(self, *args, **kwargs): class HasTokenVerification(ABCTokenVerification): - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def get_amount( self, user_address: str, token_address: None | str, token_client: TokenClient ) -> int: @@ -117,9 +108,6 @@ def get_amount( class AllowListVerification(ConstraintVerification): _param_keys = [ConstraintParam.CSV_FILE] - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs): file_path = self.param_values[ConstraintParam.CSV_FILE.name] self.allow_list = [] diff --git a/core/constraints/gitcoin_passport.py b/core/constraints/gitcoin_passport.py index 0c45246..89d72b6 100644 --- a/core/constraints/gitcoin_passport.py +++ b/core/constraints/gitcoin_passport.py @@ -15,9 +15,6 @@ class HasGitcoinPassportProfile(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.GITCOIN_PASSPORT.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import GitcoinPassportConnection @@ -37,9 +34,6 @@ class HasMinimumHumanityScore(ConstraintVerification): _param_keys = [ConstraintParam.MINIMUM] app_name = ConstraintApp.GITCOIN_PASSPORT.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import GitcoinPassportConnection @@ -72,9 +66,6 @@ class HasDonatedOnGitcoin(ConstraintVerification): app_name = ConstraintApp.GITCOIN_PASSPORT.value _graph_url = "https://grants-stack-indexer-v2.gitcoin.co" - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: try: return self.has_donated( diff --git a/core/constraints/lens.py b/core/constraints/lens.py index 28a2e6e..4c017c1 100644 --- a/core/constraints/lens.py +++ b/core/constraints/lens.py @@ -12,9 +12,6 @@ class HasLensProfile(ConstraintVerification): _param_keys = [] app_name = ConstraintApp.LENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import LensConnection @@ -31,9 +28,6 @@ class IsFollowingLensUser(ConstraintVerification): _param_keys = [ConstraintParam.LENS_PROFILE_ID] app_name = ConstraintApp.LENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import LensConnection @@ -51,9 +45,6 @@ class BeFollowedByLensUser(ConstraintVerification): _param_keys = [ConstraintParam.LENS_PROFILE_ID] app_name = ConstraintApp.LENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import LensConnection @@ -73,9 +64,6 @@ class DidMirrorOnLensPublication(ConstraintVerification): _param_keys = [ConstraintParam.LENS_PUBLICATION_ID] app_name = ConstraintApp.LENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import LensConnection @@ -97,9 +85,6 @@ class DidCollectLensPublication(ConstraintVerification): _param_keys = [ConstraintParam.LENS_PUBLICATION_ID] app_name = ConstraintApp.LENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import LensConnection @@ -122,9 +107,6 @@ class HasMinimumLensFollower(ConstraintVerification): _param_keys = [ConstraintParam.MINIMUM] app_name = ConstraintApp.LENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import LensConnection @@ -148,9 +130,6 @@ class HasMinimumLensPost(ConstraintVerification): _param_keys = [ConstraintParam.MINIMUM] app_name = ConstraintApp.LENS.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import LensConnection diff --git a/core/constraints/muon_node.py b/core/constraints/muon_node.py index 4ac8d0d..ccea9d5 100644 --- a/core/constraints/muon_node.py +++ b/core/constraints/muon_node.py @@ -51,10 +51,10 @@ class HasMuonNode(ConstraintVerification): CHAIN_ID = 56 CONTRACT_ADDR = "0x6eA3096eB0fAf5c1DEb970DCd29A6b10a48DaD83" - def __init__(self, user_profile) -> None: + def __init__(self, user_profile, *, obj=None) -> None: from core.models import Chain - super().__init__(user_profile) + super().__init__(user_profile, obj=obj) self.chain = Chain.objects.get(chain_id=HasMuonNode.CHAIN_ID) self.web3_utils = Web3Utils(self.chain.rpc_url_private, self.chain.poa) self.web3_utils.set_contract(HasMuonNode.CONTRACT_ADDR, MUON_NODE_MANAGER_ABI) diff --git a/core/constraints/octant.py b/core/constraints/octant.py index 15b1af7..00599dc 100644 --- a/core/constraints/octant.py +++ b/core/constraints/octant.py @@ -17,9 +17,6 @@ class GLMStakingVerification(ConstraintVerification): ] GLM_CONTRACT_ADDRESS = "0x879133Fd79b7F48CE1c368b0fCA9ea168eaF117c" - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs): from core.models import Chain diff --git a/core/constraints/optimism.py b/core/constraints/optimism.py index fe4a1c1..be182a0 100644 --- a/core/constraints/optimism.py +++ b/core/constraints/optimism.py @@ -35,9 +35,6 @@ class DelegateOP(ConstraintVerification): ] OP_TOKEN_CONTRACT = "0x4200000000000000000000000000000000000042" - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from core.models import Chain @@ -74,6 +71,3 @@ class DidDelegateOPToAddress(ConstraintVerification): ConstraintParam.MINIMUM, ConstraintParam.ADDRESS, ) - - def __init__(self, user_profile) -> None: - super().__init__(user_profile) diff --git a/core/constraints/twitter.py b/core/constraints/twitter.py index 6211800..28cd3dd 100644 --- a/core/constraints/twitter.py +++ b/core/constraints/twitter.py @@ -116,13 +116,10 @@ def is_observed(self, *args, **kwargs) -> bool: return False -class IsFollowinTwitterUser(ConstraintVerification): +class IsFollowingTwitterUser(ConstraintVerification): _param_keys = [ConstraintParam.TWITTER_USERNAME] app_name = ConstraintApp.TWITTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import TwitterConnection @@ -146,9 +143,6 @@ class BeFollowedByTwitterUser(ConstraintVerification): _param_keys = [ConstraintParam.TWITTER_USERNAME] app_name = ConstraintApp.TWITTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import TwitterConnection @@ -174,9 +168,6 @@ class DidRetweetTweet(ConstraintVerification): _param_keys = [ConstraintParam.TWEET_ID] app_name = ConstraintApp.TWITTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import TwitterConnection @@ -196,9 +187,6 @@ class DidQuoteTweet(ConstraintVerification): _param_keys = [ConstraintParam.TWEET_ID] app_name = ConstraintApp.TWITTER.value - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def is_observed(self, *args, **kwargs) -> bool: from authentication.models import TwitterConnection @@ -218,9 +206,6 @@ class IsFollowingTwitterBatch(ConstraintVerification): app_name = ConstraintApp.TWITTER.value _param_keys = [ConstraintParam.TWITTER_IDS] - def __init__(self, user_profile) -> None: - super().__init__(user_profile) - def get_info(self, *args, **kwargs) -> None | dict: from authentication.models import TwitterConnection From 5b1ef4e4620bfc12492a8c1820011cfc60ab377c Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 16 Sep 2024 17:39:36 +0000 Subject: [PATCH 063/110] fixed typo name --- core/constraints/__init__.py | 2 +- core/models.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index 74decfe..4c6b0f8 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -62,7 +62,7 @@ HasTwitter, HasVoteOnATweet, IsFollowingTwitterBatch, - IsFollowinTwitterUser, + IsFollowingTwitterUser, ) from core.constraints.zora import DidMintZoraNFT diff --git a/core/models.py b/core/models.py index dea31cb..744534f 100644 --- a/core/models.py +++ b/core/models.py @@ -53,7 +53,7 @@ IsFollowingFarcasterUser, IsFollowingLensUser, IsFollowingTwitterBatch, - IsFollowinTwitterUser, + IsFollowingTwitterUser, ) from .utils import SolanaWeb3Utils, Web3Utils @@ -146,7 +146,7 @@ class Type(models.TextChoices): HasGitcoinPassportProfile, IsFollowingFarcasterChannel, BridgeEthToArb, - IsFollowinTwitterUser, + IsFollowingTwitterUser, BeFollowedByTwitterUser, DidRetweetTweet, DidQuoteTweet, From 5788e3e205ca2017df430cb9b8351db40574ceee Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Mon, 16 Sep 2024 17:49:32 +0000 Subject: [PATCH 064/110] added migrations --- .../migrations/0080_alter_constraint_name.py | 18 ++++++++++++++++++ .../migrations/0066_alter_constraint_name.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 prizetap/migrations/0080_alter_constraint_name.py create mode 100644 tokenTap/migrations/0066_alter_constraint_name.py diff --git a/prizetap/migrations/0080_alter_constraint_name.py b/prizetap/migrations/0080_alter_constraint_name.py new file mode 100644 index 0000000..9b210dc --- /dev/null +++ b/prizetap/migrations/0080_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-09-16 17:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('prizetap', '0079_fix_raffle_image_urls'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowingTwitterUser', 'IsFollowingTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('core.DidMintZoraNFT', 'DidMintZoraNFT'), ('core.HasVerifiedHCaptcha', 'HasVerifiedHCaptcha'), ('prizetap.HaveUnitapPass', 'HaveUnitapPass'), ('prizetap.NotHaveUnitapPass', 'NotHaveUnitapPass'), ('faucet.OptimismDonationConstraint', 'OptimismDonationConstraint'), ('faucet.OptimismClaimingGasConstraint', 'OptimismClaimingGasConstraint')], max_length=255, unique=True), + ), + ] diff --git a/tokenTap/migrations/0066_alter_constraint_name.py b/tokenTap/migrations/0066_alter_constraint_name.py new file mode 100644 index 0000000..5631d1e --- /dev/null +++ b/tokenTap/migrations/0066_alter_constraint_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-09-16 17:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tokenTap', '0065_fix_token_image_prefix'), + ] + + operations = [ + migrations.AlterField( + model_name='constraint', + name='name', + field=models.CharField(choices=[('core.BrightIDMeetVerification', 'BrightIDMeetVerification'), ('core.BrightIDAuraVerification', 'BrightIDAuraVerification'), ('core.HasNFTVerification', 'HasNFTVerification'), ('core.HasTokenVerification', 'HasTokenVerification'), ('core.HasTokenTransferVerification', 'HasTokenTransferVerification'), ('core.AllowListVerification', 'AllowListVerification'), ('core.HasENSVerification', 'HasENSVerification'), ('core.HasLensProfile', 'HasLensProfile'), ('core.IsFollowingLensUser', 'IsFollowingLensUser'), ('core.BeFollowedByLensUser', 'BeFollowedByLensUser'), ('core.DidMirrorOnLensPublication', 'DidMirrorOnLensPublication'), ('core.DidCollectLensPublication', 'DidCollectLensPublication'), ('core.HasMinimumLensPost', 'HasMinimumLensPost'), ('core.HasMinimumLensFollower', 'HasMinimumLensFollower'), ('core.BeFollowedByFarcasterUser', 'BeFollowedByFarcasterUser'), ('core.HasMinimumFarcasterFollower', 'HasMinimumFarcasterFollower'), ('core.DidLikedFarcasterCast', 'DidLikedFarcasterCast'), ('core.DidRecastFarcasterCast', 'DidRecastFarcasterCast'), ('core.IsFollowingFarcasterUser', 'IsFollowingFarcasterUser'), ('core.HasFarcasterProfile', 'HasFarcasterProfile'), ('core.BeAttestedBy', 'BeAttestedBy'), ('core.Attest', 'Attest'), ('core.HasDonatedOnGitcoin', 'HasDonatedOnGitcoin'), ('core.HasMinimumHumanityScore', 'HasMinimumHumanityScore'), ('core.HasGitcoinPassportProfile', 'HasGitcoinPassportProfile'), ('core.IsFollowingFarcasterChannel', 'IsFollowingFarcasterChannel'), ('core.BridgeEthToArb', 'BridgeEthToArb'), ('core.IsFollowingTwitterUser', 'IsFollowingTwitterUser'), ('core.BeFollowedByTwitterUser', 'BeFollowedByTwitterUser'), ('core.DidRetweetTweet', 'DidRetweetTweet'), ('core.DidQuoteTweet', 'DidQuoteTweet'), ('core.HasMuonNode', 'HasMuonNode'), ('core.DelegateArb', 'DelegateArb'), ('core.DelegateOP', 'DelegateOP'), ('core.DidDelegateArbToAddress', 'DidDelegateArbToAddress'), ('core.DidDelegateOPToAddress', 'DidDelegateOPToAddress'), ('core.GLMStakingVerification', 'GLMStakingVerification'), ('core.IsFollowingTwitterBatch', 'IsFollowingTwitterBatch'), ('core.IsFollowingFarcasterBatch', 'IsFollowingFarcasterBatch'), ('core.HasVerifiedCloudflareCaptcha', 'HasVerifiedCloudflareCaptcha'), ('core.DidMintZoraNFT', 'DidMintZoraNFT'), ('core.HasVerifiedHCaptcha', 'HasVerifiedHCaptcha'), ('tokenTap.OncePerMonthVerification', 'OncePerMonthVerification'), ('tokenTap.OnceInALifeTimeVerification', 'OnceInALifeTimeVerification'), ('faucet.OptimismHasClaimedGasConstraint', 'OptimismHasClaimedGasConstraint')], max_length=255, unique=True), + ), + ] From 1b976516c7d4167c2c927d68d570717e195c0d2f Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Tue, 17 Sep 2024 17:03:46 +0330 Subject: [PATCH 065/110] fixed arguments passing --- core/constraints/arbitrum.py | 4 ++-- core/constraints/optimism.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/constraints/arbitrum.py b/core/constraints/arbitrum.py index bc80222..611d3d8 100644 --- a/core/constraints/arbitrum.py +++ b/core/constraints/arbitrum.py @@ -125,8 +125,8 @@ def is_observed(self, *args, **kwargs) -> bool: for user_address in self.user_addresses: try: address = token_client.to_checksum_address(user_address) - delegated_address = token_client.get_delegates_address() - if ( + delegated_address = token_client.get_delegates_address(user_address) + if not delegated_address or ( ConstraintParam.ADDRESS.name in self.param_keys() and delegated_address.lower() != self.param_values[ConstraintParam.ADDRESS.name].lower() diff --git a/core/constraints/optimism.py b/core/constraints/optimism.py index fe4a1c1..4c51c66 100644 --- a/core/constraints/optimism.py +++ b/core/constraints/optimism.py @@ -53,8 +53,8 @@ def is_observed(self, *args, **kwargs) -> bool: for user_address in self.user_addresses: try: address = token_client.to_checksum_address(user_address) - delegated_address = token_client.get_delegates_address() - if ( + delegated_address = token_client.get_delegates_address(user_address) + if not delegated_address or ( ConstraintParam.ADDRESS.name in self.param_keys() and delegated_address.lower() != self.param_values[ConstraintParam.ADDRESS.name].lower() From 2ac51b97219c735fbc03f9b8a20fc19c4fa758d2 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Wed, 18 Sep 2024 13:08:53 +0330 Subject: [PATCH 066/110] Fix farcaster following constraint --- core/thirdpartyapp/farcaster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/thirdpartyapp/farcaster.py b/core/thirdpartyapp/farcaster.py index 7e81843..b5201e7 100644 --- a/core/thirdpartyapp/farcaster.py +++ b/core/thirdpartyapp/farcaster.py @@ -134,7 +134,7 @@ def is_following(self, fid: str, address: str) -> bool: """ try: follower_fid = self._get_profile(address)["fid"] - return self._get_followers_status(follower_fid, fid) + return self._get_followers_status(follower_fid, fid)[fid] except ( RequestException, IndexError, @@ -153,7 +153,7 @@ def be_followed_by(self, fid: str, address: str): """ try: following_fid = self._get_profile(address)["fid"] - return self._get_follow_status(fid, following_fid) + return self._get_follow_status(fid, following_fid)[following_fid] except ( RequestException, IndexError, From 048353851949efb57bd27d695a662ff52f5449c0 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 21 Sep 2024 14:19:26 +0330 Subject: [PATCH 067/110] added farcaster admin conenction --- authentication/admin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/authentication/admin.py b/authentication/admin.py index 2a96c4b..f6d1871 100644 --- a/authentication/admin.py +++ b/authentication/admin.py @@ -7,6 +7,7 @@ TwitterConnection, UserProfile, Wallet, + FarcasterConnection, ) @@ -53,9 +54,15 @@ class EnsConnectionAdmin(admin.ModelAdmin): search_fields = ["user_profile__username", "user_wallet_address"] +class FarcasterConnectionAdmin(admin.ModelAdmin): + list_display = ["pk", "user_profile", "user_wallet_address"] + search_fields = ["user_profile__username", "user_wallet_address"] + + admin.site.register(Wallet, WalletAdmin) admin.site.register(UserProfile, ProfileAdmin) admin.site.register(BrightIDConnection, BrightIDConnectionAdmin) admin.site.register(GitcoinPassportConnection, GitcoinPassportConnectionAdmin) admin.site.register(TwitterConnection, TwitterConnectionAdmin) admin.site.register(ENSConnection, EnsConnectionAdmin) +admin.site.register(FarcasterConnection, FarcasterConnectionAdmin) From 9da6bbe3bf6ca168158df5a300cd2ec2b060104e Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Sun, 22 Sep 2024 00:03:36 +0330 Subject: [PATCH 068/110] Fix gitcoinPassport problem --- authentication/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authentication/models.py b/authentication/models.py index 0d1f5af..2dcb694 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -233,8 +233,8 @@ class GitcoinPassportConnection(BaseThirdPartyConnection): @property def score(self): - score_tuple = self.driver.get_score(self.user_wallet_address) - return score_tuple[0] if score_tuple else 0.0 + _score = self.driver.submit_passport(self.user_wallet_address) + return _score @receiver(pre_save, sender=GitcoinPassportConnection) From be24112d53a8eb37cad32be6b802b5cf5bf1d9d4 Mon Sep 17 00:00:00 2001 From: Pooya Fekri Date: Sun, 22 Sep 2024 00:11:23 +0330 Subject: [PATCH 069/110] Return only str in score property --- authentication/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authentication/models.py b/authentication/models.py index 2dcb694..11e79a2 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -234,7 +234,7 @@ class GitcoinPassportConnection(BaseThirdPartyConnection): @property def score(self): _score = self.driver.submit_passport(self.user_wallet_address) - return _score + return _score if _score is not None else 0 @receiver(pre_save, sender=GitcoinPassportConnection) From c53b8d834fb1414ae38a58aa77d3ae9a25cc2022 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 5 Oct 2024 13:05:23 +0330 Subject: [PATCH 070/110] added telegram application --- authentication/models.py | 8 ++++++++ brightIDfaucet/settings.py | 7 ++++--- core/thirdpartyapp/telegram.py | 24 ++++++++++++++++++++++++ telegram/__init__.py | 0 telegram/admin.py | 0 telegram/apps.py | 6 ++++++ telegram/bot.py | 5 +++++ telegram/migrations/__init__.py | 0 telegram/models.py | 0 telegram/urls.py | 11 +++++++++++ telegram/views.py | 0 11 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 core/thirdpartyapp/telegram.py create mode 100644 telegram/__init__.py create mode 100644 telegram/admin.py create mode 100644 telegram/apps.py create mode 100644 telegram/bot.py create mode 100644 telegram/migrations/__init__.py create mode 100644 telegram/models.py create mode 100644 telegram/urls.py create mode 100644 telegram/views.py diff --git a/authentication/models.py b/authentication/models.py index 11e79a2..3b71244 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -250,6 +250,14 @@ def submit_passport(sender, instance: GitcoinPassportConnection, **kwargs): raise GitcoinPassportSaveError("Gitcoin passport not exists.") +class TelegramConnection(BaseThirdPartyConnection): + title = "Telegram" + user_id = models.BigIntegerField() + first_name = models.CharField(null=True, blank=True, max_length=255) + last_name = models.CharField(null=True, blank=True, max_length=255) + username = models.CharField(null=True, blank=True, max_length=600) + + class TwitterConnection(BaseThirdPartyConnection): title = "Twitter" oauth_token = models.CharField(max_length=255, unique=True, blank=False, null=False) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index bafc6cb..86b9210 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -135,6 +135,7 @@ def before_send(event, hint): "corsheaders", "django_filters", "safedelete", + "django_telegram_login", ] MIDDLEWARE = [ @@ -171,7 +172,7 @@ def before_send(event, hint): WSGI_APPLICATION = "brightIDfaucet.wsgi.application" STORAGES = { - "default": { + "default": { "BACKEND": "cloudflare_images.storage.CloudflareImagesStorage", }, "staticfiles": { # default @@ -251,8 +252,8 @@ def before_send(event, hint): # These headers are required for Cloudflare and HCaptcha Turnstile anti-bot service CORS_ALLOW_HEADERS = list(default_headers) + [ - 'cf-turnstile-response', - 'hc-turnstile-response', + "cf-turnstile-response", + "hc-turnstile-response", ] # Static files (CSS, JavaScript, Images) diff --git a/core/thirdpartyapp/telegram.py b/core/thirdpartyapp/telegram.py new file mode 100644 index 0000000..5e988a9 --- /dev/null +++ b/core/thirdpartyapp/telegram.py @@ -0,0 +1,24 @@ +from django_telegram_login.authentication import verify_telegram_authentication +from django_telegram_login.widgets.generator import create_redirect_login_widget + +from django.conf import settings + + +class TelegramUtil: + bot_token = settings.TELEGRAM_BOT_API_KEY + bot_username = settings.TELEGRAM_BOT_USERNAME + + def __init__(self) -> None: + pass + + def login_user(self, telegram_data): + is_verified = verify_telegram_authentication( + bot_token=self.bot_token, request_data=telegram_data + ) + + def create_telegram_widget(self, redirect_url): + telegram_login_widget = create_redirect_login_widget( + redirect_url, self.bot_username + ) + + return telegram_login_widget diff --git a/telegram/__init__.py b/telegram/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/admin.py b/telegram/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/apps.py b/telegram/apps.py new file mode 100644 index 0000000..75e5a1c --- /dev/null +++ b/telegram/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TelegramConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "telegram" diff --git a/telegram/bot.py b/telegram/bot.py new file mode 100644 index 0000000..04de599 --- /dev/null +++ b/telegram/bot.py @@ -0,0 +1,5 @@ +import telebot +from django.conf import settings + + +bot = telebot.TeleBot(settings.TELEGRAM_BOT_API_KEY) diff --git a/telegram/migrations/__init__.py b/telegram/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/models.py b/telegram/models.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/urls.py b/telegram/urls.py new file mode 100644 index 0000000..bd3ebc0 --- /dev/null +++ b/telegram/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + + +urlpatterns = [ + path("telegram/login/", TelegramLoginView.as_view(), name="telegram-login"), + path( + "telegram/login/callback/", + TelegramLoginCallbackView.as_view(), + name="telegram-login-callback", + ), +] diff --git a/telegram/views.py b/telegram/views.py new file mode 100644 index 0000000..e69de29 From eb6c0e16b278b6fc650e1dedccf3096138c3f331 Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 08:53:45 +0330 Subject: [PATCH 071/110] Update settings --- brightIDfaucet/settings.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index bafc6cb..d70dfaf 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -171,7 +171,7 @@ def before_send(event, hint): WSGI_APPLICATION = "brightIDfaucet.wsgi.application" STORAGES = { - "default": { + "default": { "BACKEND": "cloudflare_images.storage.CloudflareImagesStorage", }, "staticfiles": { # default @@ -179,7 +179,6 @@ def before_send(event, hint): }, } - # Database DATABASES = {"default": dj_database_url.config(conn_max_age=600)} @@ -192,23 +191,32 @@ def before_send(event, hint): # } # } -CACHES = { - "default": { - "BACKEND": "django_bmemcached.memcached.BMemcached", - "LOCATION": MEMCACHED_URL.split(","), - "OPTIONS": { - "username": MEMCACHED_USERNAME, - "password": MEMCACHED_PASSWORD, - }, +if ',' in MEMCACHED_URL: + CACHES = { + "default": { + "BACKEND": "django_bmemcached.memcached.BMemcached", + "LOCATION": MEMCACHED_URL.split(","), + "OPTIONS": { + "username": MEMCACHED_USERNAME, + "password": MEMCACHED_PASSWORD, + }, + } } -} +else: + CACHES = { + "default": { + "BACKEND": "django_bmemcached.memcached.BMemcached", + "LOCATION": MEMCACHED_URL + } + } + # Password validation # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation.Us" - "erAttributeSimilarityValidator", + "erAttributeSimilarityValidator", }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", @@ -246,7 +254,6 @@ def before_send(event, hint): else: CORS_ALLOW_ALL_ORIGINS = True - # Add Turnstile response headers for CORS # These headers are required for Cloudflare and HCaptcha Turnstile anti-bot service From c0c9e687294ffc9af498c604ebfc581875e758f7 Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 09:08:16 +0330 Subject: [PATCH 072/110] Add dev deploy action --- .github/workflows/dev-deploy.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/dev-deploy.yml diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml new file mode 100644 index 0000000..6a9ab3a --- /dev/null +++ b/.github/workflows/dev-deploy.yml @@ -0,0 +1,22 @@ +name: 'dev-deploy' + +on: + push: + branches: + - develop + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Cloning repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Push to dokku + uses: dokku/github-action@master + with: + git_remote_url: 'ssh://dokku@dokku.me:22/appname' + ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} + branch: 'develop' \ No newline at end of file From 7ccce05499cc855ed97c679ef3d96cf60025c4ba Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 09:11:21 +0330 Subject: [PATCH 073/110] Update dev deploy action --- .github/workflows/dev-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index 6a9ab3a..1d218e1 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -17,6 +17,6 @@ jobs: - name: Push to dokku uses: dokku/github-action@master with: - git_remote_url: 'ssh://dokku@dokku.me:22/appname' + git_remote_url: 'ssh://dokku@70.34.210.129:unitap-dev' ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} branch: 'develop' \ No newline at end of file From 89044d8878661346520b6fbb849587bab8254dbf Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 09:16:51 +0330 Subject: [PATCH 074/110] Update settings --- brightIDfaucet/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index d70dfaf..e4eaafc 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -191,7 +191,7 @@ def before_send(event, hint): # } # } -if ',' in MEMCACHED_URL: +if MEMCACHED_URL and ',' in MEMCACHED_URL: CACHES = { "default": { "BACKEND": "django_bmemcached.memcached.BMemcached", From b69547d714ec2b32e5a864a0e30abaa8bb706433 Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 09:19:32 +0330 Subject: [PATCH 075/110] Update dev-deploy action --- .github/workflows/dev-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index 1d218e1..3bbe175 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -17,6 +17,6 @@ jobs: - name: Push to dokku uses: dokku/github-action@master with: - git_remote_url: 'ssh://dokku@70.34.210.129:unitap-dev' + git_remote_url: 'ssh://dokku@70.34.210.129:80/unitap-dev' ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} branch: 'develop' \ No newline at end of file From efcfd1bc6ffac9beba13e3b5c5ce6eb29c6e1747 Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 10:24:59 +0330 Subject: [PATCH 076/110] Update dev-deploy action --- .github/workflows/dev-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index 3bbe175..3c3ad55 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -17,6 +17,6 @@ jobs: - name: Push to dokku uses: dokku/github-action@master with: - git_remote_url: 'ssh://dokku@70.34.210.129:80/unitap-dev' + git_remote_url: 'ssh://dokku@70.34.210.129/unitap-dev' ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} branch: 'develop' \ No newline at end of file From ff556801533709bf341acc5b319d19e1740520be Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 10:37:14 +0330 Subject: [PATCH 077/110] Update dev-deploy action --- .github/workflows/dev-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index 3c3ad55..793b44d 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -17,6 +17,6 @@ jobs: - name: Push to dokku uses: dokku/github-action@master with: - git_remote_url: 'ssh://dokku@70.34.210.129/unitap-dev' + git_remote_url: 'dokku@70.34.210.129:unitap-dev' ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} branch: 'develop' \ No newline at end of file From 1bc12f3dc6de51e8dd931f4414dbec6134562a6d Mon Sep 17 00:00:00 2001 From: MMD Date: Tue, 8 Oct 2024 10:40:03 +0330 Subject: [PATCH 078/110] Update dev-deploy action --- .github/workflows/dev-deploy.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index 793b44d..9c142d1 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -1,4 +1,4 @@ -name: 'dev-deploy' +name: Deploy to Server on: push: @@ -8,15 +8,23 @@ on: jobs: deploy: runs-on: ubuntu-latest + steps: - - name: Cloning repo - uses: actions/checkout@v4 - with: - fetch-depth: 0 + - name: Checkout code + uses: actions/checkout@v2 - - name: Push to dokku - uses: dokku/github-action@master + - name: Set up SSH + uses: webfactory/ssh-agent@v0.5.3 with: - git_remote_url: 'dokku@70.34.210.129:unitap-dev' - ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} - branch: 'develop' \ No newline at end of file + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Disable Host Key Checking + run: | + mkdir -p ~/.ssh + echo "StrictHostKeyChecking no" >> ~/.ssh/config + + - name: Push to Server + run: | + git remote add dokku dokku@70.34.210.129:unitap-dev + git fetch --unshallow origin + git push dokku develop -f \ No newline at end of file From 7d7831668515982b45130ae21b97f59a4db713d9 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 10 Oct 2024 00:35:06 +0330 Subject: [PATCH 079/110] added telegram connection implementation updated requirements.txt --- .../migrations/0043_telegramconnection.py | 29 +++++++++++++++++ authentication/models.py | 3 ++ brightIDfaucet/settings.py | 2 ++ brightIDfaucet/urls.py | 2 ++ core/thirdpartyapp/telegram.py | 2 +- requirements.txt | 2 ++ telegram/urls.py | 5 +-- telegram/views.py | 31 +++++++++++++++++++ 8 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 authentication/migrations/0043_telegramconnection.py diff --git a/authentication/migrations/0043_telegramconnection.py b/authentication/migrations/0043_telegramconnection.py new file mode 100644 index 0000000..d276eeb --- /dev/null +++ b/authentication/migrations/0043_telegramconnection.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0 on 2024-10-05 10:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0042_twitterconnection_twitter_id'), + ] + + operations = [ + migrations.CreateModel( + name='TelegramConnection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('user_id', models.BigIntegerField()), + ('first_name', models.CharField(blank=True, max_length=255, null=True)), + ('last_name', models.CharField(blank=True, max_length=255, null=True)), + ('username', models.CharField(blank=True, max_length=600, null=True)), + ('user_profile', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s', to='authentication.userprofile')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/authentication/models.py b/authentication/models.py index 3b71244..832ede3 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -257,6 +257,9 @@ class TelegramConnection(BaseThirdPartyConnection): last_name = models.CharField(null=True, blank=True, max_length=255) username = models.CharField(null=True, blank=True, max_length=600) + def is_connected(self): + return True + class TwitterConnection(BaseThirdPartyConnection): title = "Twitter" diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 86b9210..e263b8f 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -73,6 +73,8 @@ def str2bool(v): MEMCACHED_USERNAME = os.environ.get("MEMCACHEDCLOUD_USERNAME") MEMCACHED_PASSWORD = os.environ.get("MEMCACHEDCLOUD_PASSWORD") DEPLOYMENT_ENV = os.environ.get("DEPLOYMENT_ENV") +TELEGRAM_BOT_API_KEY = os.environ.get("TELEGRAM_BOT_API_KEY") +TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME") CLOUDFLARE_IMAGES_ACCOUNT_ID = os.environ.get("CLOUDFLARE_ACCOUNT_ID") CLOUDFLARE_IMAGES_API_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN") diff --git a/brightIDfaucet/urls.py b/brightIDfaucet/urls.py index 37a86bd..e73acb5 100644 --- a/brightIDfaucet/urls.py +++ b/brightIDfaucet/urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import include, path @@ -31,4 +32,5 @@ path("api/prizetap/", include("prizetap.urls")), path("api/quiztap/", include("quiztap.urls")), path("api/analytics/", include("analytics.urls")), + path("api/telegram/", include("telegram.urls")), ] diff --git a/core/thirdpartyapp/telegram.py b/core/thirdpartyapp/telegram.py index 5e988a9..b4ebef7 100644 --- a/core/thirdpartyapp/telegram.py +++ b/core/thirdpartyapp/telegram.py @@ -11,7 +11,7 @@ class TelegramUtil: def __init__(self) -> None: pass - def login_user(self, telegram_data): + def verify_login(self, telegram_data): is_verified = verify_telegram_authentication( bot_token=self.bot_token, request_data=telegram_data ) diff --git a/requirements.txt b/requirements.txt index 958b6a6..1fcff10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,5 @@ ratelimit~=2.2.1 pillow==10.4.0 django-cloudflare-images~=0.6.0 zstandard~=0.17.0 +pyTelegramBotAPI==4.23.0 +django-telegram-login==0.2.3 \ No newline at end of file diff --git a/telegram/urls.py b/telegram/urls.py index bd3ebc0..7bb2ce1 100644 --- a/telegram/urls.py +++ b/telegram/urls.py @@ -1,10 +1,11 @@ from django.urls import path +from .views import TelegramLoginCallbackView, TelegramLoginView urlpatterns = [ - path("telegram/login/", TelegramLoginView.as_view(), name="telegram-login"), + path("login/", TelegramLoginView.as_view(), name="telegram-login"), path( - "telegram/login/callback/", + "login/callback/", TelegramLoginCallbackView.as_view(), name="telegram-login-callback", ), diff --git a/telegram/views.py b/telegram/views.py index e69de29..1981463 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -0,0 +1,31 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from django_telegram_login.widgets.generator import create_redirect_login_widget +from django_telegram_login.authentication import verify_telegram_authentication +from django.conf import settings + +from core.thirdpartyapp.telegram import TelegramUtil + + +class TelegramLoginView(APIView): + def get(self, request): + telegram_login_widget = TelegramUtil().create_telegram_widget( + "/api/telegram/login/callback" + ) + return Response({"login_url": telegram_login_widget}) + + +class TelegramLoginCallbackView(APIView): + def post(self, request): + telegram_data = request.data + is_verified = TelegramUtil().verify_login(telegram_data) + + if is_verified: + user_id = telegram_data["id"] + username = telegram_data["username"] + + return Response( + {"status": "success", "user_id": user_id, "username": username} + ) + else: + return Response({"status": "error"}, status=400) From 1dbfb7c34e3a70c1c62879df88decdc30bbf236a Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 13 Oct 2024 15:53:30 +0330 Subject: [PATCH 080/110] added autocomplete fields for admin panel --- authentication/admin.py | 6 ++++++ prizetap/admin.py | 7 +++++-- tokenTap/admin.py | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/authentication/admin.py b/authentication/admin.py index f6d1871..45463db 100644 --- a/authentication/admin.py +++ b/authentication/admin.py @@ -23,6 +23,7 @@ class ProfileAdmin(admin.ModelAdmin): class WalletAdmin(admin.ModelAdmin): list_display = ["pk", "wallet_type", "user_profile"] + autocomplete_fields = ["user_profile"] search_fields = [ "user_profile__initial_context_id", "wallet_type", @@ -34,6 +35,7 @@ class WalletAdmin(admin.ModelAdmin): class BrightIDConnectionAdmin(admin.ModelAdmin): list_display = ["pk", "user_profile", "context_id", "age"] + autocomplete_fields = ["user_profile"] search_fields = [ "context_id", ] @@ -42,21 +44,25 @@ class BrightIDConnectionAdmin(admin.ModelAdmin): class GitcoinPassportConnectionAdmin(admin.ModelAdmin): list_display = ["pk", "user_profile", "user_wallet_address"] search_fields = ["user_wallet_address", "user_profile__username"] + autocomplete_fields = ["user_profile"] class TwitterConnectionAdmin(admin.ModelAdmin): list_display = ["pk", "user_profile", "oauth_token"] search_fields = ["user_profile__username", "oauth_token"] + autocomplete_fields = ["user_profile"] class EnsConnectionAdmin(admin.ModelAdmin): list_display = ["pk", "user_profile", "user_wallet_address"] search_fields = ["user_profile__username", "user_wallet_address"] + autocomplete_fields = ["user_profile"] class FarcasterConnectionAdmin(admin.ModelAdmin): list_display = ["pk", "user_profile", "user_wallet_address"] search_fields = ["user_profile__username", "user_wallet_address"] + autocomplete_fields = ["user_profile"] admin.site.register(Wallet, WalletAdmin) diff --git a/prizetap/admin.py b/prizetap/admin.py index f32a74d..8aa3754 100644 --- a/prizetap/admin.py +++ b/prizetap/admin.py @@ -7,9 +7,10 @@ class RaffleAdmin(admin.ModelAdmin): list_display = ["pk", "name", "creator_name", "status"] readonly_fields = ["vrf_tx_hash"] + autocomplete_fields = ["creator_profile"] -class RaffleٍEntryAdmin(admin.ModelAdmin): +class RaffleEntryAdmin(admin.ModelAdmin): list_display = [ "pk", "raffle", @@ -17,13 +18,15 @@ class RaffleٍEntryAdmin(admin.ModelAdmin): "tx_hash", "age", ] + autocomplete_fields = ["wallet_address"] class LineaRaffleEntriesAdmin(admin.ModelAdmin): list_display = ["pk", "wallet_address", "is_winner"] + autocomplete_fields = ["wallet_address"] admin.site.register(Raffle, RaffleAdmin) -admin.site.register(RaffleEntry, RaffleٍEntryAdmin) +admin.site.register(RaffleEntry, RaffleEntryAdmin) admin.site.register(Constraint, UserConstraintBaseAdmin) admin.site.register(LineaRaffleEntries, LineaRaffleEntriesAdmin) diff --git a/tokenTap/admin.py b/tokenTap/admin.py index b43bed5..803d176 100644 --- a/tokenTap/admin.py +++ b/tokenTap/admin.py @@ -36,6 +36,7 @@ class TokenDistributionClaimAdmin(admin.ModelAdmin): ] search_fields = ["user_wallet_address"] list_filter = ["token_distribution", "status"] + autocomplete_fields = ["user_profile"] class GlobalSettingsAdmin(admin.ModelAdmin): From 73cfdbeafd61c86ab0689fa6958be9352c424dc3 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Tue, 15 Oct 2024 09:34:05 +0330 Subject: [PATCH 081/110] removed extra autocomplete field --- prizetap/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/prizetap/admin.py b/prizetap/admin.py index 8aa3754..f19c540 100644 --- a/prizetap/admin.py +++ b/prizetap/admin.py @@ -23,7 +23,6 @@ class RaffleEntryAdmin(admin.ModelAdmin): class LineaRaffleEntriesAdmin(admin.ModelAdmin): list_display = ["pk", "wallet_address", "is_winner"] - autocomplete_fields = ["wallet_address"] admin.site.register(Raffle, RaffleAdmin) From 3f57ea86c373be7527b723354e54869775ecaad2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Tue, 15 Oct 2024 10:34:58 +0330 Subject: [PATCH 082/110] removed unused imports --- brightIDfaucet/settings.py | 1 - telegram/views.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index e263b8f..a0f5785 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -137,7 +137,6 @@ def before_send(event, hint): "corsheaders", "django_filters", "safedelete", - "django_telegram_login", ] MIDDLEWARE = [ diff --git a/telegram/views.py b/telegram/views.py index 1981463..a8e6914 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -1,7 +1,5 @@ from rest_framework.views import APIView from rest_framework.response import Response -from django_telegram_login.widgets.generator import create_redirect_login_widget -from django_telegram_login.authentication import verify_telegram_authentication from django.conf import settings from core.thirdpartyapp.telegram import TelegramUtil From d6ffc3b38297ef99ed42597873910c7bbc83d303 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Tue, 15 Oct 2024 10:35:34 +0330 Subject: [PATCH 083/110] fixed errors --- prizetap/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/prizetap/admin.py b/prizetap/admin.py index f19c540..c8579c0 100644 --- a/prizetap/admin.py +++ b/prizetap/admin.py @@ -18,7 +18,6 @@ class RaffleEntryAdmin(admin.ModelAdmin): "tx_hash", "age", ] - autocomplete_fields = ["wallet_address"] class LineaRaffleEntriesAdmin(admin.ModelAdmin): From 57ff9e57454acd181886968651fe69bfeeaafd3d Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Wed, 16 Oct 2024 17:56:30 +0330 Subject: [PATCH 084/110] added telegram app --- authentication/models.py | 11 - brightIDfaucet/settings.py | 3 + core/thirdpartyapp/telegram.py | 35 ++- requirements.txt | 2 +- telegram/apps.py | 13 ++ telegram/bot.py | 340 +++++++++++++++++++++++++++++- telegram/messages/__init__.py | 26 +++ telegram/messages/about.py | 0 telegram/messages/easter_egg.py | 0 telegram/messages/gastap_stats.py | 9 + telegram/messages/menu.py | 0 telegram/models.py | 16 ++ telegram/tests.py | 0 telegram/urls.py | 4 +- telegram/views.py | 64 +++++- 15 files changed, 494 insertions(+), 29 deletions(-) create mode 100644 telegram/messages/__init__.py create mode 100644 telegram/messages/about.py create mode 100644 telegram/messages/easter_egg.py create mode 100644 telegram/messages/gastap_stats.py create mode 100644 telegram/messages/menu.py create mode 100644 telegram/tests.py diff --git a/authentication/models.py b/authentication/models.py index 832ede3..11e79a2 100644 --- a/authentication/models.py +++ b/authentication/models.py @@ -250,17 +250,6 @@ def submit_passport(sender, instance: GitcoinPassportConnection, **kwargs): raise GitcoinPassportSaveError("Gitcoin passport not exists.") -class TelegramConnection(BaseThirdPartyConnection): - title = "Telegram" - user_id = models.BigIntegerField() - first_name = models.CharField(null=True, blank=True, max_length=255) - last_name = models.CharField(null=True, blank=True, max_length=255) - username = models.CharField(null=True, blank=True, max_length=600) - - def is_connected(self): - return True - - class TwitterConnection(BaseThirdPartyConnection): title = "Twitter" oauth_token = models.CharField(max_length=255, unique=True, blank=False, null=False) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index a0f5785..c14a69c 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -73,8 +73,11 @@ def str2bool(v): MEMCACHED_USERNAME = os.environ.get("MEMCACHEDCLOUD_USERNAME") MEMCACHED_PASSWORD = os.environ.get("MEMCACHEDCLOUD_PASSWORD") DEPLOYMENT_ENV = os.environ.get("DEPLOYMENT_ENV") + + TELEGRAM_BOT_API_KEY = os.environ.get("TELEGRAM_BOT_API_KEY") TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME") +TELEGRAM_BOT_API_SECRET = os.environ.get("TELEGRAM_BOT_API_SECRET") CLOUDFLARE_IMAGES_ACCOUNT_ID = os.environ.get("CLOUDFLARE_ACCOUNT_ID") CLOUDFLARE_IMAGES_API_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN") diff --git a/core/thirdpartyapp/telegram.py b/core/thirdpartyapp/telegram.py index b4ebef7..285a3f6 100644 --- a/core/thirdpartyapp/telegram.py +++ b/core/thirdpartyapp/telegram.py @@ -1,8 +1,33 @@ -from django_telegram_login.authentication import verify_telegram_authentication -from django_telegram_login.widgets.generator import create_redirect_login_widget - from django.conf import settings +import hashlib +import hmac +import time + + +def verify_telegram_auth(bot_token, data): + auth_data = dict(data) + hash_check = auth_data.pop("hash") + + # Create the data string by sorting keys and concatenating key=value pairs + data_check_string = "\n".join([f"{k}={v}" for k, v in sorted(auth_data.items())]) + + # Hash the data string with your bot's token + secret_key = hashlib.sha256(bot_token.encode()).digest() + calculated_hash = hmac.new( + secret_key, data_check_string.encode(), hashlib.sha256 + ).hexdigest() + + # Compare the calculated hash with the received hash + if calculated_hash != hash_check: + return False + + # Optional: Check that the authentication data is recent (within a day) + if time.time() - int(auth_data["auth_date"]) > 86400: + return False + + return True + class TelegramUtil: bot_token = settings.TELEGRAM_BOT_API_KEY @@ -12,9 +37,7 @@ def __init__(self) -> None: pass def verify_login(self, telegram_data): - is_verified = verify_telegram_authentication( - bot_token=self.bot_token, request_data=telegram_data - ) + return verify_telegram_auth(self.bot_token, telegram_data) def create_telegram_widget(self, redirect_url): telegram_login_widget = create_redirect_login_widget( diff --git a/requirements.txt b/requirements.txt index 1fcff10..d8b205d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bip-utils==2.3.0 -django==5 +django==5.1.2 anchorpy==0.15.0 djangorestframework==3.15.2 djangorestframework-camel-case==1.3.0 diff --git a/telegram/apps.py b/telegram/apps.py index 75e5a1c..afe6606 100644 --- a/telegram/apps.py +++ b/telegram/apps.py @@ -1,6 +1,19 @@ from django.apps import AppConfig +from django.conf import settings class TelegramConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "telegram" + + def ready(self) -> None: + if settings.DEPLOYMENT_ENV == "DEV": + return super().ready() + + from .bot import TelegramMessenger + + messenger = TelegramMessenger.get_instance() + messenger.ensure_webhook() + messenger.ready() + + return super().ready() diff --git a/telegram/bot.py b/telegram/bot.py index 04de599..72de17f 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -1,5 +1,343 @@ -import telebot +from authentication.models import UserProfile from django.conf import settings +from abc import ABC, abstractmethod +from telebot import types + +import telebot +import time +import logging +logger = logging.getLogger(__name__) + bot = telebot.TeleBot(settings.TELEGRAM_BOT_API_KEY) + +MAX_REQUESTS_PER_SECOND = 30 + + +class TelegramRateLimiter: + """ + A rate limiter class for controlling the frequency of Telegram API requests. + + This class ensures that the number of API requests sent per second does not exceed + the defined maximum rate by enforcing a delay between requests when needed. + + Attributes: + first_request_time (float): The time the first request in the current batch was sent. + requests_sent (int): The number of requests sent in the current batch. + """ + + def __init__(self, max_requests_per_second=MAX_REQUESTS_PER_SECOND): + """ + Initialize the rate limiter with the maximum number of requests per second. + + Args: + max_requests_per_second (int): The maximum number of requests allowed per second. + """ + self.first_request_time = 0.0 + self.requests_sent = 0 + self.max_requests_per_second = max_requests_per_second + + def send_telegram_request(self, func, *args, **kwargs): + """ + Send a Telegram API request, enforcing rate limits. + + This method calculates the elapsed time since the first request, and if the number of + requests exceeds the allowed rate, it introduces a delay to avoid hitting the limit. + + Args: + func (callable): The function that sends the actual Telegram API request. + *args: Positional arguments to pass to the `func`. + **kwargs: Keyword arguments to pass to the `func`. + + Returns: + The result of the API request or `None` if an exception occurs. + """ + current_time = time.time() + elapsed_time = current_time - self.first_request_time + + # Check if we've sent too many requests within the current second + if self.requests_sent >= self.max_requests_per_second: + # Calculate the delay needed to respect the rate limit + delay_time = max(1 - elapsed_time, 0) + time.sleep(delay_time) + # Reset the request counter and time after waiting + self.requests_sent = 0 + self.first_request_time = time.time() + + try: + # Attempt to execute the provided function (API request) + result = func(*args, **kwargs) + except Exception as e: + # Handle any exceptions that occur during the API request + logger.error(f"[T] Exception occurred while making the request: {e}") + result = None + + # Reset the timer for the first request in the next batch if necessary + if self.requests_sent == 0: + self.first_request_time = time.time() + + # Increment the count of requests sent + self.requests_sent += 1 + + return result + + +class TelegramMessenger: + """ + A singleton class responsible for managing Telegram bot interactions, including message, + command, and callback handling. It uses rate limiting to ensure requests stay within Telegram's + API limits. + """ + + # Rate limiter to prevent exceeding Telegram API limits + limiter = TelegramRateLimiter() + + # Singleton instance of TelegramMessenger + instance = None + + # Handlers for different types of bot interactions + command_handlers: dict[str, "BaseTelegramCommandHandler"] = {} + message_handlers: dict[str, "BaseTelegramMessageHandler"] = {} + callback_handlers: dict[str, "BaseTelegramCallbackHandler"] = {} + + # List of admin users (can be extended to validate admin access) + admin_users = [] + + def __init__(self) -> None: + """Private constructor for singleton behavior.""" + pass + + @staticmethod + def get_instance(): + """ + Get the singleton instance of TelegramMessenger. If it doesn't exist, create it. + + Returns: + TelegramMessenger: The singleton instance. + """ + if TelegramMessenger.instance is None: + TelegramMessenger.instance = TelegramMessenger() + + return TelegramMessenger.instance + + def handle_user_message(self, message: types.Message): + """ + Handle a regular user message by dispatching it to the appropriate handler. + + Args: + message (types.Message): The incoming message from the user. + + Returns: + The result of the handler or None if no handler exists for the message. + """ + instance = self.message_handlers.get(message.text) + + if not instance: + return None + + return instance.handler(message) + + def handle_user_command(self, message: types.Message): + """ + Handle a user command message (starting with '/') by dispatching it to the appropriate handler. + + Args: + message (types.Message): The incoming command message. + + Returns: + The result of the handler or None if no handler exists for the command. + """ + args = message.text[1:].split() # Split command and arguments + command = args[0] + + instance = self.command_handlers.get(command) + + if not instance: + return None + + return instance.handler(message, command, args[1:]) + + def handle_callback_query(self, call: types.CallbackQuery): + """ + Handle a callback query (typically from inline buttons) by dispatching it to the appropriate handler. + + Args: + call (types.CallbackQuery): The incoming callback query. + + Returns: + The result of the handler or None if no handler exists for the callback data. + """ + cmd_parts = call.data.split(",") # Assume callback data is comma-separated + + instance = self.callback_handlers.get(cmd_parts[0]) + + if not instance: + return None + + return instance.handler(call) + + def on_telegram_message(self, message: types.Message): + """ + Handle an incoming Telegram message by determining if it's a command or a regular message. + + Args: + message (types.Message): The incoming message. + + Returns: + The result of either a command handler or message handler. + """ + if message.text.startswith("/"): + return self.handle_user_command(message) + + return self.handle_user_message(message) + + def ensure_webhook(self): + """ + Ensure that the webhook is correctly set up for receiving Telegram updates. + Uses the Telegram Bot API to register the webhook URL. + """ + import requests + + # Replace with your webhook and Telegram Bot token + webhook_url = "https://api.unitap.app/api/telegram/wh/" + telegram_api_url = f"https://api.telegram.org/bot{bot.token}/setWebhook" + + # Register webhook with secret token for added security + requests.post( + telegram_api_url, + data={"url": webhook_url, "secret_token": settings.TELEGRAM_BOT_API_SECRET}, + ) + + def ready(self): + """ + Prepare the bot by registering the message and callback handlers for processing updates. + This is the setup function that connects Telegram message updates with handler functions. + """ + bot.message_handler(func=lambda _: True)( + lambda message: self.on_telegram_message(message) + ) + + bot.callback_query_handler(func=lambda _: True)( + lambda call: self.handle_callback_query(call) + ) + + def send_message(self, user: UserProfile, *args, **kwargs) -> types.Message: + """ + Send a message to a user using Telegram bot API with rate limiting. + + Args: + user (UserProfile): The user profile to whom the message will be sent. + *args: Positional arguments to pass to the bot.send_message function. + **kwargs: Keyword arguments to pass to the bot.send_message function. + + Returns: + types.Message: The sent message object. + + Raises: + ValueError: If the user's Telegram connection is not available. + """ + if not user.telegramconnections: + raise ValueError("[T] User telegram connection must be present") + + return self.limiter.send_telegram_request( + bot.send_message, chat_id=user.telegramconnections.user_id, *args, **kwargs + ) + + def update_query_messages(self, call: types.CallbackQuery, text: str, markup): + """ + Update the text and markup of an existing inline query message (typically from inline buttons). + + Args: + call (types.CallbackQuery): The callback query that triggered this action. + text (str): The updated text for the message. + markup: The updated inline keyboard markup for the message. + + Returns: + The result of the bot.edit_message_text method. + """ + return self.limiter.send_telegram_request( + bot.edit_message_text, + chat_id=call.message.chat.id, + message_id=call.message.message_id, + text=text, + reply_markup=markup, + ) + + def reply_to(self, *args, **kwargs): + """ + Send a reply to a message using the Telegram bot API with rate limiting. + + Args: + *args: Positional arguments to pass to the bot.reply_to function. + **kwargs: Keyword arguments to pass to the bot.reply_to function. + + Returns: + The result of the bot.reply_to method. + """ + return self.limiter.send_telegram_request(bot.reply_to, *args, **kwargs) + + +class TelegramEventHandler: + + messenger = TelegramMessenger.get_instance() + + def __init__(self, bot: telebot.TeleBot): + self.bot = bot + + def handler(self, message: types.Message): + raise NotImplementedError("[T] Subclasses should implement the handler method") + + +class BaseTelegramCommandHandler(TelegramEventHandler): + """ + Base class for Telegram message handlers. + Subclasses should define the command and implement the handler. + """ + + command = None + required_role = None + + def handler(self, message: types.Message, command: str, args: list[str]): + raise NotImplementedError("[T] Subclasses should implement the handler method") + + +class BaseTelegramMessageHandler(TelegramEventHandler): + message = None + required_role = None + + +class BaseTelegramCallbackHandler(TelegramEventHandler): + callback = None + required_role = None + params = [] + + def handler(self, callback: types.CallbackQuery): + raise NotImplementedError("[T] Subclasses should implement the handler method") + + +def register_message_handlers(): + handlers = {} + for subclass in BaseTelegramMessageHandler.__subclasses__(): + if subclass.message: + handlers[subclass.message] = subclass(bot) + + return handlers + + +def register_callback_handlers(): + handlers = {} + for subclass in BaseTelegramCallbackHandler.__subclasses__(): + if subclass.callback: + handlers[subclass.callback] = subclass(bot) + + return handlers + + +def register_command_handlers(): + handlers = {} + for subclass in BaseTelegramCommandHandler.__subclasses__(): + if subclass.command: + handlers[subclass.command] = subclass(bot) + + return handlers diff --git a/telegram/messages/__init__.py b/telegram/messages/__init__.py new file mode 100644 index 0000000..83639e4 --- /dev/null +++ b/telegram/messages/__init__.py @@ -0,0 +1,26 @@ +from telegram.bot import BaseTelegramCommandHandler +from telebot import types + + +class StartCommandHandler(BaseTelegramCommandHandler): + command = "start" + + def handler(self, message: types.Message, command: str, args: list[str]): + if args: + return + + markup = types.ReplyKeyboardMarkup() + + markup.add(types.KeyboardButton("Connect your account")) + + markup.add(types.KeyboardButton("Stats of gastap")) + + markup.add(types.KeyboardButton("Report bug and get reward")) + + markup.add(types.KeyboardButton("About Unitap")) + + self.messenger.reply_to( + message, + "Welcome to unitap official telegram bot, how can i help you?", + reply_markup=markup, + ) diff --git a/telegram/messages/about.py b/telegram/messages/about.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/messages/easter_egg.py b/telegram/messages/easter_egg.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/messages/gastap_stats.py b/telegram/messages/gastap_stats.py new file mode 100644 index 0000000..468710a --- /dev/null +++ b/telegram/messages/gastap_stats.py @@ -0,0 +1,9 @@ +from telegram.bot import BaseTelegramMessageHandler +from telebot import types + + +class StartCommandHandler(BaseTelegramMessageHandler): + message = "Stats of gastap" + + def handler(self, message: types.Message): + pass diff --git a/telegram/messages/menu.py b/telegram/messages/menu.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/models.py b/telegram/models.py index e69de29..9152d2f 100644 --- a/telegram/models.py +++ b/telegram/models.py @@ -0,0 +1,16 @@ +from typing_extensions import override +from django.db import models +from authentication.models import BaseThirdPartyConnection + + +class TelegramConnection(BaseThirdPartyConnection): + title = "Telegram" + user_id = models.BigIntegerField() + first_name = models.CharField(null=True, blank=True, max_length=255) + last_name = models.CharField(null=True, blank=True, max_length=255) + username = models.CharField(null=True, blank=True, max_length=600) + is_collected_easter_egg = models.BooleanField(default=False) + + @override + def is_connected(self): + return True diff --git a/telegram/tests.py b/telegram/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/telegram/urls.py b/telegram/urls.py index 7bb2ce1..4652d1b 100644 --- a/telegram/urls.py +++ b/telegram/urls.py @@ -3,10 +3,12 @@ urlpatterns = [ - path("login/", TelegramLoginView.as_view(), name="telegram-login"), path( "login/callback/", TelegramLoginCallbackView.as_view(), name="telegram-login-callback", ), + path( + "wh/", + ), ] diff --git a/telegram/views.py b/telegram/views.py index a8e6914..b5fbd6f 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -1,19 +1,65 @@ -from rest_framework.views import APIView +from rest_framework.views import CreateApiView from rest_framework.response import Response -from django.conf import settings +from telegram.models import TelegramConnection from core.thirdpartyapp.telegram import TelegramUtil +from .bot import tbot + +from django.core.cache import cache +from django.conf import settings +from django.http import HttpResponse +from django.core.exceptions import PermissionDenied +from django.views.decorators.csrf import csrf_exempt + + +import telebot +import requests + + +def get_telegram_safe_ips(): + telegram_ips_cache = cache.get("telegram_safe_ip_list") + + if telegram_ips_cache: + return telegram_ips_cache + + telegram_ips = requests.get("https://core.telegram.org/bots/webhooks").json()[ + "ip_ranges" + ] + + cache.set("telegram_safe_ip_list", telegram_ips, 800) + + return telegram_ips + + +@csrf_exempt +def telebot_respond(request): + client_ip = request.META["REMOTE_ADDR"] + + telegram_ips = get_telegram_safe_ips + + # Validate the request's IP address against Telegram's IP ranges + if client_ip not in telegram_ips: + raise PermissionDenied("Invalid IP address") + + if ( + request.headers.get("X-Telegram-Bot-Api-Secret-Token") + != settings.TELEGRAM_BOT_API_SECRET + ): + raise PermissionDenied("Invalid secret token") + + if request.META["CONTENT_TYPE"] == "application/json": + json_data = request.body.decode("utf-8") + update = telebot.types.Update.de_json(json_data) + tbot.process_new_updates([update]) + return HttpResponse("") + + else: + raise PermissionDenied -class TelegramLoginView(APIView): - def get(self, request): - telegram_login_widget = TelegramUtil().create_telegram_widget( - "/api/telegram/login/callback" - ) - return Response({"login_url": telegram_login_widget}) +class TelegramLoginCallbackView(CreateApiView): -class TelegramLoginCallbackView(APIView): def post(self, request): telegram_data = request.data is_verified = TelegramUtil().verify_login(telegram_data) From 3280a74eea606ec357de61a5c94880057840f3e6 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Wed, 16 Oct 2024 17:56:39 +0330 Subject: [PATCH 085/110] added telegram to installed apps --- brightIDfaucet/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index c14a69c..16e8a5a 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -140,6 +140,7 @@ def before_send(event, hint): "corsheaders", "django_filters", "safedelete", + "telegram.apps.TelegramConfig", ] MIDDLEWARE = [ From 77f8fa433f1b0843bb08b2a6c6beea397aaec436 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 14:17:10 +0330 Subject: [PATCH 086/110] added bug report handler --- brightIDfaucet/settings.py | 1 + telegram/bot.py | 59 ++++++++++++--- telegram/messages/__init__.py | 30 +------- telegram/messages/about.py | 25 +++++++ telegram/messages/bug_reporter.py | 118 ++++++++++++++++++++++++++++++ telegram/messages/gastap_stats.py | 37 +++++++++- telegram/messages/start.py | 26 +++++++ telegram/serializers.py | 18 +++++ telegram/views.py | 43 ++++++++--- 9 files changed, 308 insertions(+), 49 deletions(-) create mode 100644 telegram/messages/bug_reporter.py create mode 100644 telegram/messages/start.py create mode 100644 telegram/serializers.py diff --git a/brightIDfaucet/settings.py b/brightIDfaucet/settings.py index 16e8a5a..483af96 100644 --- a/brightIDfaucet/settings.py +++ b/brightIDfaucet/settings.py @@ -78,6 +78,7 @@ def str2bool(v): TELEGRAM_BOT_API_KEY = os.environ.get("TELEGRAM_BOT_API_KEY") TELEGRAM_BOT_USERNAME = os.environ.get("TELEGRAM_BOT_USERNAME") TELEGRAM_BOT_API_SECRET = os.environ.get("TELEGRAM_BOT_API_SECRET") +TELEGRAM_BUG_REPORTER_CHANNEL_ID = os.environ.get("TELEGRAM_BUG_REPORTER_CHANNEL_ID") CLOUDFLARE_IMAGES_ACCOUNT_ID = os.environ.get("CLOUDFLARE_ACCOUNT_ID") CLOUDFLARE_IMAGES_API_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN") diff --git a/telegram/bot.py b/telegram/bot.py index 72de17f..13d9481 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -7,10 +7,12 @@ import time import logging +from telegram.models import TelegramConnection + logger = logging.getLogger(__name__) -bot = telebot.TeleBot(settings.TELEGRAM_BOT_API_KEY) +telebot_instance = telebot.TeleBot(settings.TELEGRAM_BOT_API_KEY) MAX_REQUESTS_PER_SECOND = 30 @@ -201,7 +203,9 @@ def ensure_webhook(self): # Replace with your webhook and Telegram Bot token webhook_url = "https://api.unitap.app/api/telegram/wh/" - telegram_api_url = f"https://api.telegram.org/bot{bot.token}/setWebhook" + telegram_api_url = ( + f"https://api.telegram.org/bot{telebot_instance.token}/setWebhook" + ) # Register webhook with secret token for added security requests.post( @@ -214,15 +218,31 @@ def ready(self): Prepare the bot by registering the message and callback handlers for processing updates. This is the setup function that connects Telegram message updates with handler functions. """ - bot.message_handler(func=lambda _: True)( + telebot_instance.message_handler(func=lambda _: True)( lambda message: self.on_telegram_message(message) ) - bot.callback_query_handler(func=lambda _: True)( + telebot_instance.callback_query_handler(func=lambda _: True)( lambda call: self.handle_callback_query(call) ) - def send_message(self, user: UserProfile, *args, **kwargs) -> types.Message: + def send_message(self, *args, **kwargs): + return self.limiter.send_telegram_request( + telebot_instance.send_message, + *args, + **kwargs, + ) + + def send_photo(self, *args, **kwargs): + return self.limiter.send_telegram_request( + telebot_instance.send_photo, + *args, + **kwargs, + ) + + def send_message_with_profile( + self, user: UserProfile, *args, **kwargs + ) -> types.Message: """ Send a message to a user using Telegram bot API with rate limiting. @@ -241,7 +261,10 @@ def send_message(self, user: UserProfile, *args, **kwargs) -> types.Message: raise ValueError("[T] User telegram connection must be present") return self.limiter.send_telegram_request( - bot.send_message, chat_id=user.telegramconnections.user_id, *args, **kwargs + telebot_instance.send_message, + chat_id=user.telegramconnections.user_id, + *args, + **kwargs, ) def update_query_messages(self, call: types.CallbackQuery, text: str, markup): @@ -257,7 +280,7 @@ def update_query_messages(self, call: types.CallbackQuery, text: str, markup): The result of the bot.edit_message_text method. """ return self.limiter.send_telegram_request( - bot.edit_message_text, + telebot_instance.edit_message_text, chat_id=call.message.chat.id, message_id=call.message.message_id, text=text, @@ -275,7 +298,9 @@ def reply_to(self, *args, **kwargs): Returns: The result of the bot.reply_to method. """ - return self.limiter.send_telegram_request(bot.reply_to, *args, **kwargs) + return self.limiter.send_telegram_request( + telebot_instance.reply_to, *args, **kwargs + ) class TelegramEventHandler: @@ -288,6 +313,18 @@ def __init__(self, bot: telebot.TeleBot): def handler(self, message: types.Message): raise NotImplementedError("[T] Subclasses should implement the handler method") + def get_user(self, user_id): + telegram_connection = TelegramConnection.objects.filter(user_id=user_id).first() + + return None if telegram_connection is None else telegram_connection.user_profile + + def register_next_step_handler( + self, message: types.Message, callback, *args, **kwargs + ): + return self.messenger.limiter.send_telegram_request( + self.bot.register_next_step_handler, message, callback, *args, **kwargs + ) + class BaseTelegramCommandHandler(TelegramEventHandler): """ @@ -320,7 +357,7 @@ def register_message_handlers(): handlers = {} for subclass in BaseTelegramMessageHandler.__subclasses__(): if subclass.message: - handlers[subclass.message] = subclass(bot) + handlers[subclass.message] = subclass(telebot_instance) return handlers @@ -329,7 +366,7 @@ def register_callback_handlers(): handlers = {} for subclass in BaseTelegramCallbackHandler.__subclasses__(): if subclass.callback: - handlers[subclass.callback] = subclass(bot) + handlers[subclass.callback] = subclass(telebot_instance) return handlers @@ -338,6 +375,6 @@ def register_command_handlers(): handlers = {} for subclass in BaseTelegramCommandHandler.__subclasses__(): if subclass.command: - handlers[subclass.command] = subclass(bot) + handlers[subclass.command] = subclass(telebot_instance) return handlers diff --git a/telegram/messages/__init__.py b/telegram/messages/__init__.py index 83639e4..5278d96 100644 --- a/telegram/messages/__init__.py +++ b/telegram/messages/__init__.py @@ -1,26 +1,4 @@ -from telegram.bot import BaseTelegramCommandHandler -from telebot import types - - -class StartCommandHandler(BaseTelegramCommandHandler): - command = "start" - - def handler(self, message: types.Message, command: str, args: list[str]): - if args: - return - - markup = types.ReplyKeyboardMarkup() - - markup.add(types.KeyboardButton("Connect your account")) - - markup.add(types.KeyboardButton("Stats of gastap")) - - markup.add(types.KeyboardButton("Report bug and get reward")) - - markup.add(types.KeyboardButton("About Unitap")) - - self.messenger.reply_to( - message, - "Welcome to unitap official telegram bot, how can i help you?", - reply_markup=markup, - ) +from .about import AboutMessageHandler +from .gastap_stats import GastapStatsHandler +from .start import StartCommandHandler +from .bug_reporter import * diff --git a/telegram/messages/about.py b/telegram/messages/about.py index e69de29..7c2febc 100644 --- a/telegram/messages/about.py +++ b/telegram/messages/about.py @@ -0,0 +1,25 @@ +from telegram.bot import BaseTelegramMessageHandler +from telebot import types + + +about_text = """**About Unitap** + +Welcome to Unitap, your smart companion for managing tasks, getting updates, and automating processes! Whether you're working on a project, organizing events, or just need help staying on top of things, Unitap is here to assist. + +With Unitap, you can: +- **Receive timely notifications** for important events. +- **Submit and track issues** directly within your workspace. +- **Connect with services** and streamline your workflow. +- **Ask for help or request hints** to navigate challenges. + +Unitap is designed to integrate seamlessly with your tools, making your work life smoother and more efficient. Start interacting today by typing `/help` to see available commands! + +read more here https://unitap.app/about +""" + + +class AboutMessageHandler(BaseTelegramMessageHandler): + message = "About Unitap ❓" + + def handler(self, message: types.Message): + self.messenger.reply_to(message, text=about_text, parse_mode="MarkdownV2") diff --git a/telegram/messages/bug_reporter.py b/telegram/messages/bug_reporter.py new file mode 100644 index 0000000..33ddec3 --- /dev/null +++ b/telegram/messages/bug_reporter.py @@ -0,0 +1,118 @@ +from telegram.bot import BaseTelegramMessageHandler, BaseTelegramCallbackHandler +from telebot import types +from django.conf import settings + + +class BugReportHandler(BaseTelegramMessageHandler): + message = "Report a bug πŸͺ²" + + def handler(self, message: types.Message): + if self.get_user(message.from_user.id) is None: + self.messenger.reply_to( + message, text="Please connect your unitap account first" + ) + + return + + self.messenger.reply_to(message, text="Please describe the bug in detail.") + + self.register_next_step_handler(message, self.ask_for_bug_details) + + def ask_for_bug_details(self, message: types.Message): + user_bug_details = message.text + self.messenger.reply_to( + message, text="How did the bug occur? Describe the steps." + ) + + # Save bug details and move to the next step (asking for how the bug occurred) + self.register_next_step_handler( + message, self.ask_for_bug_occurence, user_bug_details + ) + + def ask_for_bug_occurence(self, message: types.Message, user_bug_details): + bug_occurence_details = message.text + + self.messenger.reply_to( + message, + text="Please upload an image related to the bug (optional, or send an empty message).", + ) + # Save the bug occurrence details and ask for an image + self.register_next_step_handler( + message, self.collect_image, user_bug_details, bug_occurence_details + ) + + def collect_image( + self, message: types.Message, user_bug_details, bug_occurence_details + ): + image = message.photo[-1].file_id if message.photo else None + self.forward_to_private_channel( + message, user_bug_details, bug_occurence_details, image + ) + + self.messenger.reply_to( + message, + text="Thank you for providing the information required for us to track the bug down\nWe will investigate and check back to you if the bug is valid and reward you with 1 extra chance at prizetap raffles", + ) + + # Step 3: Forward the bug report to a private channel with reward button + def forward_to_private_channel( + self, message: types.Message, user_bug_details, bug_occurence_details, image + ): + private_channel_id = ( + settings.TELEGRAM_BUG_REPORTER_CHANNEL_ID + ) # Make sure this is set in Django settings + + # Construct message to forward to the private channel + bug_report_message = ( + f"*Bug Report*\n" + f"User Id: {message.from_user.id}\n" + f"From: @{message.chat.username}\n\n" + f"*Bug Description:* {user_bug_details}\n" + f"*Steps to Reproduce:* {bug_occurence_details}\n\n" + f"Unitap user id {self.get_user(message.from_user.id).pk}" + ) + + # Forward the text and image if provided + if image: + self.messenger.send_photo( + chat_id=private_channel_id, + photo=image, + caption=bug_report_message, + parse_mode="MarkdownV2", + ) + else: + self.messenger.send_message( + chat_id=private_channel_id, + text=bug_report_message, + parse_mode="MarkdownV2", + ) + + reward_button = types.InlineKeyboardMarkup() + reward_button.add( + types.InlineKeyboardButton( + text="Reward User", callback_data=f"reward_{message.chat.id}" + ) + ) + + self.messenger.send_message( + chat_id=private_channel_id, + text="Click below to reward this user:", + reply_markup=reward_button, + ) + + +class ReportBugRewardHandler(BaseTelegramCallbackHandler): + callback = "reward-bug" + + def handler(self, callback: types.CallbackQuery): + user_id = next(self.params) + + user = self.get_user(user_id) + + user.prizetap_winning_chance_number += 1 + user.save() + + self.messenger.send_message( + chat_id=user_id, + text="Congraculations\n\nYou got 1 extra prizetap chance for reporting a valid bug ❀️❀️.", + ) diff --git a/telegram/messages/gastap_stats.py b/telegram/messages/gastap_stats.py index 468710a..123998a 100644 --- a/telegram/messages/gastap_stats.py +++ b/telegram/messages/gastap_stats.py @@ -1,9 +1,42 @@ +from django.template import Template, Context from telegram.bot import BaseTelegramMessageHandler from telebot import types +from faucet.models import Faucet +gastap_text = """ +*Gas Tokens Availability* β›½ -class StartCommandHandler(BaseTelegramMessageHandler): +Here are the available chains for claiming gas tokens: + +{% for faucet in faucets %} +πŸ”΅ *{{ faucet.chain.chain_name }}* +Max Claim Amount: {{ faucet.max_claim_amount }} +Available for: {% if faucet.is_one_time_claim %}One-time{% else %}Weekly{% endif %} + +{% if faucet.has_enough_funds %} +🟒 *Status:* Available +Fuel Level: {{ faucet.fuel_level }}% +{% else %} +πŸ”΄ *Status:* Out of Balance +{% endif %} + +--- +{% endfor %} + +Remember: You need BrightID verification to claim your tokens. +""" + + +class GastapStatsHandler(BaseTelegramMessageHandler): message = "Stats of gastap" def handler(self, message: types.Message): - pass + faucets = Faucet.objects.filter(is_active=True, show_in_gastap=True) + + # Prepare the template + template = Template(gastap_text) + context = Context({"faucets": faucets}) + + rendered_message = template.render(context) + + self.messenger.reply_to(message, text=rendered_message, parse_mode="MarkdownV2") diff --git a/telegram/messages/start.py b/telegram/messages/start.py new file mode 100644 index 0000000..62b7c43 --- /dev/null +++ b/telegram/messages/start.py @@ -0,0 +1,26 @@ +from telegram.bot import BaseTelegramCommandHandler +from telebot import types + + +class StartCommandHandler(BaseTelegramCommandHandler): + command = "start" + + def handler(self, message: types.Message, command: str, args: list[str]): + if args: + return + + markup = types.ReplyKeyboardMarkup() + + markup.add(types.KeyboardButton("Connect your account")) + + markup.add(types.KeyboardButton("Stats of gastap")) + + markup.add(types.KeyboardButton("Report bug πŸͺ²")) + + markup.add(types.KeyboardButton("About Unitap ❓")) + + self.messenger.reply_to( + message, + "Welcome to unitap official telegram bot, how can i help you?", + reply_markup=markup, + ) diff --git a/telegram/serializers.py b/telegram/serializers.py new file mode 100644 index 0000000..99bb07e --- /dev/null +++ b/telegram/serializers.py @@ -0,0 +1,18 @@ +from rest_framework import serializers + +from authentication.serializers import BaseThirdPartyConnectionSerializer +from telegram.models import TelegramConnection + + +class TelegramConnectionSerializer(BaseThirdPartyConnectionSerializer): + hash = serializers.CharField(write_only=True) + + class Meta: + model = TelegramConnection + fields = "__all__" + read_only_fields = [ + "created_on", + "pk", + "user_profile", + "title", + ] diff --git a/telegram/views.py b/telegram/views.py index b5fbd6f..552d344 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -1,10 +1,11 @@ -from rest_framework.views import CreateApiView +from rest_framework.generics import CreateAPIView from rest_framework.response import Response - +from rest_framework.permissions import IsAuthenticated +from telegram.serializers import TelegramConnectionSerializer from telegram.models import TelegramConnection from core.thirdpartyapp.telegram import TelegramUtil -from .bot import tbot +from .bot import telebot_instance, TelegramMessenger from django.core.cache import cache from django.conf import settings @@ -51,25 +52,47 @@ def telebot_respond(request): if request.META["CONTENT_TYPE"] == "application/json": json_data = request.body.decode("utf-8") update = telebot.types.Update.de_json(json_data) - tbot.process_new_updates([update]) + telebot_instance.process_new_updates([update]) return HttpResponse("") else: raise PermissionDenied -class TelegramLoginCallbackView(CreateApiView): +welcome_text = """*Welcome to Unitap!* πŸŽ‰ + +Your Telegram account is now successfully *connected* to Unitap. From here on, you'll receive important updates, notifications, and can interact with Unitap directly through this chat. + +Here’s what you can do: +- *Submit issues or requests* +- *Get notified about events and changes* +- *Ask for hints or help* when needed + +Type `/help` at any time to see available commands. + +Thanks for joining Unitap! We’re here to assist you in staying productive and connected. +""" - def post(self, request): - telegram_data = request.data + +class TelegramLoginCallbackView(CreateAPIView): + permission_classes = [IsAuthenticated] + serializer_class = TelegramConnectionSerializer + + @property + def user_profile(self): + return self.request.user.profile + + def perform_create(self, serializer: TelegramConnectionSerializer): + telegram_data = serializer.validated_data is_verified = TelegramUtil().verify_login(telegram_data) if is_verified: user_id = telegram_data["id"] - username = telegram_data["username"] - return Response( - {"status": "success", "user_id": user_id, "username": username} + TelegramMessenger.get_instance().send_message( + chat_id=user_id, text=welcome_text ) + + serializer.save(user_profile=self.user_profile) else: return Response({"status": "error"}, status=400) From 217e398e80626dee7740281c7da64948a71240f4 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 14:20:36 +0330 Subject: [PATCH 087/110] update sample.env --- sample.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample.env b/sample.env index 825c75b..8f51905 100644 --- a/sample.env +++ b/sample.env @@ -6,4 +6,4 @@ SECRET_KEY="django-insecure-!=_mi0j#rhk7c9p-0wg-3me6y&fk$+fahz6fh)k1n#&@s(9vf5" BRIGHT_PRIVATE_KEY="" DEBUG="True" SENTRY_DSN="DEBUG-DSN" -DEPLOYMENT_ENV="env" +DEPLOYMENT_ENV="dev" From 663a7b7cfaa937476dba4182a46f5f7aed34bfb9 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 14:32:37 +0330 Subject: [PATCH 088/110] provided more ux for telegram bot --- telegram/bot.py | 6 ++ telegram/messages/bug_reporter.py | 94 ++++++++++++++++++++++++------- telegram/messages/menu.py | 10 ++++ telegram/messages/start.py | 13 +---- telegram/views.py | 3 +- 5 files changed, 93 insertions(+), 33 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 13d9481..1b0832b 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -160,6 +160,12 @@ def handle_user_command(self, message: types.Message): return instance.handler(message, command, args[1:]) + def build_callback_string(self, command: str, args: list[str]): + if not args: + return command + + return f"{command},{",".join(args)}" + def handle_callback_query(self, call: types.CallbackQuery): """ Handle a callback query (typically from inline buttons) by dispatching it to the appropriate handler. diff --git a/telegram/messages/bug_reporter.py b/telegram/messages/bug_reporter.py index 33ddec3..e68c36f 100644 --- a/telegram/messages/bug_reporter.py +++ b/telegram/messages/bug_reporter.py @@ -7,23 +7,44 @@ class BugReportHandler(BaseTelegramMessageHandler): message = "Report a bug πŸͺ²" def handler(self, message: types.Message): + # Check if the user's Telegram account is linked to Unitap if self.get_user(message.from_user.id) is None: self.messenger.reply_to( - message, text="Please connect your unitap account first" + message, + text=( + "❌ *Your Telegram account is not connected to Unitap.*\n\n" + "To report a bug, please first connect your Telegram account to Unitap. " + "Visit the following link and log in: https://unitap.app. " + "Go to your profile and connect your Telegram account.\n\n" + "Once connected, you can report issues and help improve our platform. 😊" + ), + parse_mode="Markdown", ) - return - self.messenger.reply_to(message, text="Please describe the bug in detail.") - + # If the user is connected, start the bug reporting process + self.messenger.reply_to( + message, + text=( + "πŸͺ² *Let's start with your bug report!*\n\n" + "Please describe the issue you're facing in detail.\n" + "_Try to include as much information as possible, such as what you were doing, what you expected to happen, and what actually happened._" + ), + parse_mode="Markdown", + ) self.register_next_step_handler(message, self.ask_for_bug_details) def ask_for_bug_details(self, message: types.Message): user_bug_details = message.text self.messenger.reply_to( - message, text="How did the bug occur? Describe the steps." + message, + text=( + "πŸ”„ *Got it! Now, please describe how the bug occurred.*\n\n" + "_What were the steps you took that led to this issue?_ " + "This will help us replicate the problem and find a fix more quickly." + ), + parse_mode="Markdown", ) - # Save bug details and move to the next step (asking for how the bug occurred) self.register_next_step_handler( message, self.ask_for_bug_occurence, user_bug_details @@ -34,7 +55,13 @@ def ask_for_bug_occurence(self, message: types.Message, user_bug_details): self.messenger.reply_to( message, - text="Please upload an image related to the bug (optional, or send an empty message).", + text=( + "πŸ“Έ *Almost done!*\n\n" + "If you have any screenshots or images that can help us better understand the issue, " + "please upload them now.\n" + "_If you don't have any images, just send an empty message._" + ), + parse_mode="Markdown", ) # Save the bug occurrence details and ask for an image self.register_next_step_handler( @@ -51,7 +78,13 @@ def collect_image( self.messenger.reply_to( message, - text="Thank you for providing the information required for us to track the bug down\nWe will investigate and check back to you if the bug is valid and reward you with 1 extra chance at prizetap raffles", + text=( + "βœ… *Thank you for providing all the necessary details!*\n\n" + "We will review your report and investigate the issue. " + "If your report is valid, you will be rewarded with an *extra chance* in our Prizetap raffles πŸŽ‰.\n\n" + "_We truly appreciate your efforts in making Unitap a better platform!_" + ), + parse_mode="Markdown", ) # Step 3: Forward the bug report to a private channel with reward button @@ -60,28 +93,31 @@ def forward_to_private_channel( ): private_channel_id = ( settings.TELEGRAM_BUG_REPORTER_CHANNEL_ID - ) # Make sure this is set in Django settings + ) # Ensure this is set in Django settings + + if not private_channel_id: + return # Construct message to forward to the private channel bug_report_message = ( - f"*Bug Report*\n" - f"User Id: {message.from_user.id}\n" - f"From: @{message.chat.username}\n\n" + f"*New Bug Report*\n" + f"πŸ‘€ *User:* @{message.chat.username} (ID: {message.from_user.id})\n" + f"Unitap User ID: {self.get_user(message.from_user.id).pk}\n\n" f"*Bug Description:* {user_bug_details}\n" f"*Steps to Reproduce:* {bug_occurence_details}\n\n" - f"Unitap user id {self.get_user(message.from_user.id).pk}" + "-----------------------------------" ) # Forward the text and image if provided if image: - self.messenger.send_photo( + forwarded_message = self.messenger.send_photo( chat_id=private_channel_id, photo=image, caption=bug_report_message, parse_mode="MarkdownV2", ) else: - self.messenger.send_message( + forwarded_message = self.messenger.send_message( chat_id=private_channel_id, text=bug_report_message, parse_mode="MarkdownV2", @@ -90,12 +126,16 @@ def forward_to_private_channel( reward_button = types.InlineKeyboardMarkup() reward_button.add( types.InlineKeyboardButton( - text="Reward User", callback_data=f"reward_{message.chat.id}" + text="Reward User πŸ†", + callback_data=self.messenger.build_callback_string( + "reward-bug", [message.from_user.id] + ), ) ) - self.messenger.send_message( - chat_id=private_channel_id, + # Send the forwarded message with the reward button in the private channel + self.messenger.reply_to( + forwarded_message, text="Click below to reward this user:", reply_markup=reward_button, ) @@ -105,14 +145,26 @@ class ReportBugRewardHandler(BaseTelegramCallbackHandler): callback = "reward-bug" def handler(self, callback: types.CallbackQuery): - user_id = next(self.params) + user_id = int(self.params[0]) + # Retrieve the user and increment their prize chances user = self.get_user(user_id) - user.prizetap_winning_chance_number += 1 user.save() + # Update the message in the private channel to indicate the user has been rewarded + self.messenger.update_query_messages( + callback, "User rewarded βœ…", reply_markup=types.InlineKeyboardMarkup() + ) + + # Notify the user that they have received the reward self.messenger.send_message( chat_id=user_id, - text="Congraculations\n\nYou got 1 extra prizetap chance for reporting a valid bug ❀️❀️.", + text=( + "πŸŽ‰ *Congratulations!*\n\n" + "Your bug report has been validated, and you’ve been rewarded with an *extra chance* " + "in the Prizetap raffles 🎟️.\n\n" + "Thank you for helping us improve Unitap! πŸ’ͺ" + ), + parse_mode="Markdown", ) diff --git a/telegram/messages/menu.py b/telegram/messages/menu.py index e69de29..575f13f 100644 --- a/telegram/messages/menu.py +++ b/telegram/messages/menu.py @@ -0,0 +1,10 @@ +from telebot import types + + +home_markup = types.ReplyKeyboardMarkup() + +home_markup.add(types.KeyboardButton("Stats of gastap")) + +home_markup.add(types.KeyboardButton("Report bug πŸͺ²")) + +home_markup.add(types.KeyboardButton("About Unitap ❓")) diff --git a/telegram/messages/start.py b/telegram/messages/start.py index 62b7c43..b7f8ea7 100644 --- a/telegram/messages/start.py +++ b/telegram/messages/start.py @@ -1,5 +1,6 @@ from telegram.bot import BaseTelegramCommandHandler from telebot import types +from .menu import home_markup class StartCommandHandler(BaseTelegramCommandHandler): @@ -9,18 +10,8 @@ def handler(self, message: types.Message, command: str, args: list[str]): if args: return - markup = types.ReplyKeyboardMarkup() - - markup.add(types.KeyboardButton("Connect your account")) - - markup.add(types.KeyboardButton("Stats of gastap")) - - markup.add(types.KeyboardButton("Report bug πŸͺ²")) - - markup.add(types.KeyboardButton("About Unitap ❓")) - self.messenger.reply_to( message, "Welcome to unitap official telegram bot, how can i help you?", - reply_markup=markup, + reply_markup=home_markup, ) diff --git a/telegram/views.py b/telegram/views.py index 552d344..d7650b9 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -3,6 +3,7 @@ from rest_framework.permissions import IsAuthenticated from telegram.serializers import TelegramConnectionSerializer from telegram.models import TelegramConnection +from telegram.messages.menu import home_markup from core.thirdpartyapp.telegram import TelegramUtil from .bot import telebot_instance, TelegramMessenger @@ -90,7 +91,7 @@ def perform_create(self, serializer: TelegramConnectionSerializer): user_id = telegram_data["id"] TelegramMessenger.get_instance().send_message( - chat_id=user_id, text=welcome_text + chat_id=user_id, text=welcome_text, reply_markup=home_markup ) serializer.save(user_profile=self.user_profile) From c7878ac61a8cd68c825c69612b2d01bc87dfb755 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 14:40:26 +0330 Subject: [PATCH 089/110] added telegram form for admin panel --- telegram/admin.py | 40 +++++++++++++++++++++++++ telegram/forms.py | 5 ++++ telegram/templates/admin/broadcast.html | 8 +++++ 3 files changed, 53 insertions(+) create mode 100644 telegram/forms.py create mode 100644 telegram/templates/admin/broadcast.html diff --git a/telegram/admin.py b/telegram/admin.py index e69de29..4fe69de 100644 --- a/telegram/admin.py +++ b/telegram/admin.py @@ -0,0 +1,40 @@ +# admin.py +from django.contrib import admin +from django.urls import path +from django.shortcuts import render +from .models import TelegramConnection +from .forms import BroadcastMessageForm +from telegram.bot import TelegramMessenger + + +@admin.register(TelegramConnection) +class TelegramConnectionAdmin(admin.ModelAdmin): + list_display = ("user", "connected_at") # Adjust as per your model fields + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "broadcast/", + self.admin_site.admin_view(self.broadcast_view), + name="broadcast_message", + ), + ] + return custom_urls + urls + + def broadcast_view(self, request): + if request.method == "POST": + form = BroadcastMessageForm(request.POST) + if form.is_valid(): + message = form.cleaned_data["message"] + users = TelegramConnection.objects.all() + messenger = TelegramMessenger.get_instance() + for user in users: + messenger.send_message(user.user_id, text=message) + + self.message_user(request, "Message sent to all users!") + return render(request, "admin/broadcast.html", {"form": form}) + else: + form = BroadcastMessageForm() + + return render(request, "admin/broadcast.html", {"form": form}) diff --git a/telegram/forms.py b/telegram/forms.py new file mode 100644 index 0000000..c1ab952 --- /dev/null +++ b/telegram/forms.py @@ -0,0 +1,5 @@ +from django import forms + + +class BroadcastMessageForm(forms.Form): + message = forms.CharField(widget=forms.Textarea, label="Message") diff --git a/telegram/templates/admin/broadcast.html b/telegram/templates/admin/broadcast.html new file mode 100644 index 0000000..3e14f46 --- /dev/null +++ b/telegram/templates/admin/broadcast.html @@ -0,0 +1,8 @@ + +{% extends "admin/base_site.html" %} {% block content %} +

Broadcast Message to Telegram Users

+
+ {% csrf_token %} {{ form.as_p }} + +
+{% endblock %} From 65b60f145db50be6c7705fc97bfb1512e60353a2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 14:45:07 +0330 Subject: [PATCH 090/110] fixed rendering the string issue --- telegram/bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telegram/bot.py b/telegram/bot.py index 1b0832b..9ef0a2c 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -164,7 +164,9 @@ def build_callback_string(self, command: str, args: list[str]): if not args: return command - return f"{command},{",".join(args)}" + args_str = ",".join(args) + + return f"{command},{args_str}" def handle_callback_query(self, call: types.CallbackQuery): """ From d62fad8fa73af73153f569b9844a19eec2f18eb2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 14:59:32 +0330 Subject: [PATCH 091/110] fixed migrations fixed errors on admin display --- .../migrations/0043_telegramconnection.py | 29 ------------ telegram/admin.py | 2 +- telegram/migrations/0001_initial.py | 44 +++++++++++++++++++ telegram/models.py | 2 +- telegram/urls.py | 6 +-- 5 files changed, 48 insertions(+), 35 deletions(-) delete mode 100644 authentication/migrations/0043_telegramconnection.py create mode 100644 telegram/migrations/0001_initial.py diff --git a/authentication/migrations/0043_telegramconnection.py b/authentication/migrations/0043_telegramconnection.py deleted file mode 100644 index d276eeb..0000000 --- a/authentication/migrations/0043_telegramconnection.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.0 on 2024-10-05 10:06 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('authentication', '0042_twitterconnection_twitter_id'), - ] - - operations = [ - migrations.CreateModel( - name='TelegramConnection', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, null=True)), - ('user_id', models.BigIntegerField()), - ('first_name', models.CharField(blank=True, max_length=255, null=True)), - ('last_name', models.CharField(blank=True, max_length=255, null=True)), - ('username', models.CharField(blank=True, max_length=600, null=True)), - ('user_profile', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='%(class)s', to='authentication.userprofile')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/telegram/admin.py b/telegram/admin.py index 4fe69de..ff3b950 100644 --- a/telegram/admin.py +++ b/telegram/admin.py @@ -9,7 +9,7 @@ @admin.register(TelegramConnection) class TelegramConnectionAdmin(admin.ModelAdmin): - list_display = ("user", "connected_at") # Adjust as per your model fields + list_display = ("pk", "user_profile", "user_id") # Adjust as per your model fields def get_urls(self): urls = super().get_urls() diff --git a/telegram/migrations/0001_initial.py b/telegram/migrations/0001_initial.py new file mode 100644 index 0000000..4ae1d49 --- /dev/null +++ b/telegram/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0 on 2024-10-05 10:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentication", "0042_twitterconnection_twitter_id"), + ] + + operations = [ + migrations.CreateModel( + name="TelegramConnection", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True, null=True)), + ("user_id", models.BigIntegerField()), + ("first_name", models.CharField(blank=True, max_length=255, null=True)), + ("last_name", models.CharField(blank=True, max_length=255, null=True)), + ("username", models.CharField(blank=True, max_length=600, null=True)), + ( + "user_profile", + models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s", + to="authentication.userprofile", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/telegram/models.py b/telegram/models.py index 9152d2f..2db68bb 100644 --- a/telegram/models.py +++ b/telegram/models.py @@ -9,7 +9,7 @@ class TelegramConnection(BaseThirdPartyConnection): first_name = models.CharField(null=True, blank=True, max_length=255) last_name = models.CharField(null=True, blank=True, max_length=255) username = models.CharField(null=True, blank=True, max_length=600) - is_collected_easter_egg = models.BooleanField(default=False) + # is_collected_easter_egg = models.BooleanField(default=False) @override def is_connected(self): diff --git a/telegram/urls.py b/telegram/urls.py index 4652d1b..8734c9c 100644 --- a/telegram/urls.py +++ b/telegram/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import TelegramLoginCallbackView, TelegramLoginView +from .views import TelegramLoginCallbackView, telebot_respond urlpatterns = [ @@ -8,7 +8,5 @@ TelegramLoginCallbackView.as_view(), name="telegram-login-callback", ), - path( - "wh/", - ), + path("wh/", telebot_respond, name="telegram-update-messages"), ] From 1cc33ba5a550fb4dd0ea2900b4b645c358380f57 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:12:43 +0330 Subject: [PATCH 092/110] added telegram connection constraint --- core/constraints/__init__.py | 1 + core/constraints/telegram.py | 23 +++++ core/models.py | 4 +- core/thirdpartyapp/telegram.py | 7 -- .../migrations/0067_alter_constraint_name.py | 0 .../migrations/0081_alter_constraint_name.py | 93 ++++++++++++++++++ .../migrations/0067_alter_constraint_name.py | 95 +++++++++++++++++++ 7 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 core/constraints/telegram.py mode change 100644 => 100755 prizetap/migrations/0067_alter_constraint_name.py create mode 100755 prizetap/migrations/0081_alter_constraint_name.py create mode 100755 tokenTap/migrations/0067_alter_constraint_name.py diff --git a/core/constraints/__init__.py b/core/constraints/__init__.py index 4c6b0f8..603f5b3 100644 --- a/core/constraints/__init__.py +++ b/core/constraints/__init__.py @@ -65,6 +65,7 @@ IsFollowingTwitterUser, ) from core.constraints.zora import DidMintZoraNFT +from core.constraints.telegram import HasTelegramConnection def get_constraint(constraint_label: str) -> ConstraintVerification: diff --git a/core/constraints/telegram.py b/core/constraints/telegram.py new file mode 100644 index 0000000..554645c --- /dev/null +++ b/core/constraints/telegram.py @@ -0,0 +1,23 @@ +import logging + +from core.constraints.abstract import ( + ConstraintApp, + ConstraintVerification, +) + + +logger = logging.getLogger(__name__) + + +class HasTelegramConnection(ConstraintVerification): + _param_keys = [] + app_name = ConstraintApp.GENERAL.value + + def is_observed(self, *args, **kwargs) -> bool: + from telegram.models import TelegramConnection + + try: + twitter = TelegramConnection.get_connection(self.user_profile) + except TelegramConnection.DoesNotExist: + return False + return twitter.is_connected() diff --git a/core/models.py b/core/models.py index 744534f..76bccf6 100644 --- a/core/models.py +++ b/core/models.py @@ -54,6 +54,7 @@ IsFollowingLensUser, IsFollowingTwitterBatch, IsFollowingTwitterUser, + HasTelegramConnection, ) from .utils import SolanaWeb3Utils, Web3Utils @@ -160,7 +161,8 @@ class Type(models.TextChoices): IsFollowingFarcasterBatch, HasVerifiedCloudflareCaptcha, DidMintZoraNFT, - HasVerifiedHCaptcha + HasVerifiedHCaptcha, + HasTelegramConnection, ] name = models.CharField( diff --git a/core/thirdpartyapp/telegram.py b/core/thirdpartyapp/telegram.py index 285a3f6..ed44f32 100644 --- a/core/thirdpartyapp/telegram.py +++ b/core/thirdpartyapp/telegram.py @@ -38,10 +38,3 @@ def __init__(self) -> None: def verify_login(self, telegram_data): return verify_telegram_auth(self.bot_token, telegram_data) - - def create_telegram_widget(self, redirect_url): - telegram_login_widget = create_redirect_login_widget( - redirect_url, self.bot_username - ) - - return telegram_login_widget diff --git a/prizetap/migrations/0067_alter_constraint_name.py b/prizetap/migrations/0067_alter_constraint_name.py old mode 100644 new mode 100755 diff --git a/prizetap/migrations/0081_alter_constraint_name.py b/prizetap/migrations/0081_alter_constraint_name.py new file mode 100755 index 0000000..6d6c5d3 --- /dev/null +++ b/prizetap/migrations/0081_alter_constraint_name.py @@ -0,0 +1,93 @@ +# Generated by Django 5.1.2 on 2024-10-17 11:39 + +from django.db import migrations, models + + +def create_prizetap_constraint(apps, schema_editor): + Constraint = apps.get_model("prizetap", "Constraint") + + Constraint.objects.create( + name="core.HasTelegramConnection", + description="HasTelegramConnection", + title="Connect Telegram", + type="VER", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("prizetap", "0080_alter_constraint_name"), + ] + + operations = [ + migrations.AlterField( + model_name="constraint", + name="name", + field=models.CharField( + choices=[ + ("core.BrightIDMeetVerification", "BrightIDMeetVerification"), + ("core.BrightIDAuraVerification", "BrightIDAuraVerification"), + ("core.HasNFTVerification", "HasNFTVerification"), + ("core.HasTokenVerification", "HasTokenVerification"), + ( + "core.HasTokenTransferVerification", + "HasTokenTransferVerification", + ), + ("core.AllowListVerification", "AllowListVerification"), + ("core.HasENSVerification", "HasENSVerification"), + ("core.HasLensProfile", "HasLensProfile"), + ("core.IsFollowingLensUser", "IsFollowingLensUser"), + ("core.BeFollowedByLensUser", "BeFollowedByLensUser"), + ("core.DidMirrorOnLensPublication", "DidMirrorOnLensPublication"), + ("core.DidCollectLensPublication", "DidCollectLensPublication"), + ("core.HasMinimumLensPost", "HasMinimumLensPost"), + ("core.HasMinimumLensFollower", "HasMinimumLensFollower"), + ("core.BeFollowedByFarcasterUser", "BeFollowedByFarcasterUser"), + ("core.HasMinimumFarcasterFollower", "HasMinimumFarcasterFollower"), + ("core.DidLikedFarcasterCast", "DidLikedFarcasterCast"), + ("core.DidRecastFarcasterCast", "DidRecastFarcasterCast"), + ("core.IsFollowingFarcasterUser", "IsFollowingFarcasterUser"), + ("core.HasFarcasterProfile", "HasFarcasterProfile"), + ("core.BeAttestedBy", "BeAttestedBy"), + ("core.Attest", "Attest"), + ("core.HasDonatedOnGitcoin", "HasDonatedOnGitcoin"), + ("core.HasMinimumHumanityScore", "HasMinimumHumanityScore"), + ("core.HasGitcoinPassportProfile", "HasGitcoinPassportProfile"), + ("core.IsFollowingFarcasterChannel", "IsFollowingFarcasterChannel"), + ("core.BridgeEthToArb", "BridgeEthToArb"), + ("core.IsFollowingTwitterUser", "IsFollowingTwitterUser"), + ("core.BeFollowedByTwitterUser", "BeFollowedByTwitterUser"), + ("core.DidRetweetTweet", "DidRetweetTweet"), + ("core.DidQuoteTweet", "DidQuoteTweet"), + ("core.HasMuonNode", "HasMuonNode"), + ("core.DelegateArb", "DelegateArb"), + ("core.DelegateOP", "DelegateOP"), + ("core.DidDelegateArbToAddress", "DidDelegateArbToAddress"), + ("core.DidDelegateOPToAddress", "DidDelegateOPToAddress"), + ("core.GLMStakingVerification", "GLMStakingVerification"), + ("core.IsFollowingTwitterBatch", "IsFollowingTwitterBatch"), + ("core.IsFollowingFarcasterBatch", "IsFollowingFarcasterBatch"), + ( + "core.HasVerifiedCloudflareCaptcha", + "HasVerifiedCloudflareCaptcha", + ), + ("core.DidMintZoraNFT", "DidMintZoraNFT"), + ("core.HasVerifiedHCaptcha", "HasVerifiedHCaptcha"), + ("core.HasTelegramConnection", "HasTelegramConnection"), + ("prizetap.HaveUnitapPass", "HaveUnitapPass"), + ("prizetap.NotHaveUnitapPass", "NotHaveUnitapPass"), + ("faucet.OptimismDonationConstraint", "OptimismDonationConstraint"), + ( + "faucet.OptimismClaimingGasConstraint", + "OptimismClaimingGasConstraint", + ), + ], + max_length=255, + unique=True, + ), + ), + migrations.RunPython( + create_prizetap_constraint, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/tokenTap/migrations/0067_alter_constraint_name.py b/tokenTap/migrations/0067_alter_constraint_name.py new file mode 100755 index 0000000..4a158fc --- /dev/null +++ b/tokenTap/migrations/0067_alter_constraint_name.py @@ -0,0 +1,95 @@ +# Generated by Django 5.1.2 on 2024-10-17 11:39 + +from django.db import migrations, models + + +def create_tokentap_constraint(apps, schema_editor): + Constraint = apps.get_model("tokenTap", "Constraint") + + Constraint.objects.create( + name="core.HasTelegramConnection", + description="HasTelegramConnection", + title="Connect Telegram", + type="VER", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("tokenTap", "0066_alter_constraint_name"), + ] + + operations = [ + migrations.AlterField( + model_name="constraint", + name="name", + field=models.CharField( + choices=[ + ("core.BrightIDMeetVerification", "BrightIDMeetVerification"), + ("core.BrightIDAuraVerification", "BrightIDAuraVerification"), + ("core.HasNFTVerification", "HasNFTVerification"), + ("core.HasTokenVerification", "HasTokenVerification"), + ( + "core.HasTokenTransferVerification", + "HasTokenTransferVerification", + ), + ("core.AllowListVerification", "AllowListVerification"), + ("core.HasENSVerification", "HasENSVerification"), + ("core.HasLensProfile", "HasLensProfile"), + ("core.IsFollowingLensUser", "IsFollowingLensUser"), + ("core.BeFollowedByLensUser", "BeFollowedByLensUser"), + ("core.DidMirrorOnLensPublication", "DidMirrorOnLensPublication"), + ("core.DidCollectLensPublication", "DidCollectLensPublication"), + ("core.HasMinimumLensPost", "HasMinimumLensPost"), + ("core.HasMinimumLensFollower", "HasMinimumLensFollower"), + ("core.BeFollowedByFarcasterUser", "BeFollowedByFarcasterUser"), + ("core.HasMinimumFarcasterFollower", "HasMinimumFarcasterFollower"), + ("core.DidLikedFarcasterCast", "DidLikedFarcasterCast"), + ("core.DidRecastFarcasterCast", "DidRecastFarcasterCast"), + ("core.IsFollowingFarcasterUser", "IsFollowingFarcasterUser"), + ("core.HasFarcasterProfile", "HasFarcasterProfile"), + ("core.BeAttestedBy", "BeAttestedBy"), + ("core.Attest", "Attest"), + ("core.HasDonatedOnGitcoin", "HasDonatedOnGitcoin"), + ("core.HasMinimumHumanityScore", "HasMinimumHumanityScore"), + ("core.HasGitcoinPassportProfile", "HasGitcoinPassportProfile"), + ("core.IsFollowingFarcasterChannel", "IsFollowingFarcasterChannel"), + ("core.BridgeEthToArb", "BridgeEthToArb"), + ("core.IsFollowingTwitterUser", "IsFollowingTwitterUser"), + ("core.BeFollowedByTwitterUser", "BeFollowedByTwitterUser"), + ("core.DidRetweetTweet", "DidRetweetTweet"), + ("core.DidQuoteTweet", "DidQuoteTweet"), + ("core.HasMuonNode", "HasMuonNode"), + ("core.DelegateArb", "DelegateArb"), + ("core.DelegateOP", "DelegateOP"), + ("core.DidDelegateArbToAddress", "DidDelegateArbToAddress"), + ("core.DidDelegateOPToAddress", "DidDelegateOPToAddress"), + ("core.GLMStakingVerification", "GLMStakingVerification"), + ("core.IsFollowingTwitterBatch", "IsFollowingTwitterBatch"), + ("core.IsFollowingFarcasterBatch", "IsFollowingFarcasterBatch"), + ( + "core.HasVerifiedCloudflareCaptcha", + "HasVerifiedCloudflareCaptcha", + ), + ("core.DidMintZoraNFT", "DidMintZoraNFT"), + ("core.HasVerifiedHCaptcha", "HasVerifiedHCaptcha"), + ("core.HasTelegramConnection", "HasTelegramConnection"), + ("tokenTap.OncePerMonthVerification", "OncePerMonthVerification"), + ( + "tokenTap.OnceInALifeTimeVerification", + "OnceInALifeTimeVerification", + ), + ( + "faucet.OptimismHasClaimedGasConstraint", + "OptimismHasClaimedGasConstraint", + ), + ], + max_length=255, + unique=True, + ), + ), + migrations.RunPython( + create_tokentap_constraint, reverse_code=migrations.RunPython.noop + ), + ] From e519bb6d3cfacd53c6faec4422b2a4cc82d03ec2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:26:38 +0330 Subject: [PATCH 093/110] fixed refactoring suggestions --- core/thirdpartyapp/telegram.py | 6 +----- telegram/admin.py | 8 ++++++++ .../0002_alter_telegramconnection_options.py | 17 +++++++++++++++++ telegram/models.py | 5 +++++ 4 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 telegram/migrations/0002_alter_telegramconnection_options.py diff --git a/core/thirdpartyapp/telegram.py b/core/thirdpartyapp/telegram.py index ed44f32..89453e9 100644 --- a/core/thirdpartyapp/telegram.py +++ b/core/thirdpartyapp/telegram.py @@ -22,11 +22,7 @@ def verify_telegram_auth(bot_token, data): if calculated_hash != hash_check: return False - # Optional: Check that the authentication data is recent (within a day) - if time.time() - int(auth_data["auth_date"]) > 86400: - return False - - return True + return time.time() - int(auth_data["auth_date"]) <= 86400 class TelegramUtil: diff --git a/telegram/admin.py b/telegram/admin.py index ff3b950..e786696 100644 --- a/telegram/admin.py +++ b/telegram/admin.py @@ -5,6 +5,7 @@ from .models import TelegramConnection from .forms import BroadcastMessageForm from telegram.bot import TelegramMessenger +from django.core.exceptions import PermissionDenied @admin.register(TelegramConnection) @@ -22,7 +23,14 @@ def get_urls(self): ] return custom_urls + urls + @admin.action( + description="Broadcast message to all users", + permissions=["telegram.can_broadcast"], + ) def broadcast_view(self, request): + if not request.user.has_perm("telegram.can_broadcast"): + raise PermissionDenied("You do not have permission to broadcast messages.") + if request.method == "POST": form = BroadcastMessageForm(request.POST) if form.is_valid(): diff --git a/telegram/migrations/0002_alter_telegramconnection_options.py b/telegram/migrations/0002_alter_telegramconnection_options.py new file mode 100644 index 0000000..64e909e --- /dev/null +++ b/telegram/migrations/0002_alter_telegramconnection_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-17 11:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('telegram', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='telegramconnection', + options={'permissions': [('can_broadcast', 'Can broadcast messages')]}, + ), + ] diff --git a/telegram/models.py b/telegram/models.py index 2db68bb..db34540 100644 --- a/telegram/models.py +++ b/telegram/models.py @@ -14,3 +14,8 @@ class TelegramConnection(BaseThirdPartyConnection): @override def is_connected(self): return True + + class Meta: + permissions = [ + ("can_broadcast", "Can broadcast messages"), + ] From eecab6c4c2762b1a9d71cb6660ae52c12cf140d3 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:32:45 +0330 Subject: [PATCH 094/110] refactored code --- telegram/bot.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 9ef0a2c..f478ca1 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -362,27 +362,24 @@ def handler(self, callback: types.CallbackQuery): def register_message_handlers(): - handlers = {} - for subclass in BaseTelegramMessageHandler.__subclasses__(): - if subclass.message: - handlers[subclass.message] = subclass(telebot_instance) - - return handlers + return { + subclass.callback: subclass(telebot_instance) + for subclass in BaseTelegramMessageHandler.__subclasses__() + if subclass.callback + } def register_callback_handlers(): - handlers = {} - for subclass in BaseTelegramCallbackHandler.__subclasses__(): - if subclass.callback: - handlers[subclass.callback] = subclass(telebot_instance) - - return handlers + return { + subclass.callback: subclass(telebot_instance) + for subclass in BaseTelegramCallbackHandler.__subclasses__() + if subclass.callback + } def register_command_handlers(): - handlers = {} - for subclass in BaseTelegramCommandHandler.__subclasses__(): - if subclass.command: - handlers[subclass.command] = subclass(telebot_instance) - - return handlers + return { + subclass.callback: subclass(telebot_instance) + for subclass in BaseTelegramCommandHandler.__subclasses__() + if subclass.callback + } From cdabbcbe8da13b3da21bae93441e47912261c6e7 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:33:35 +0330 Subject: [PATCH 095/110] refactored code --- telegram/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/telegram/views.py b/telegram/views.py index d7650b9..de6fbd6 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -20,9 +20,7 @@ def get_telegram_safe_ips(): - telegram_ips_cache = cache.get("telegram_safe_ip_list") - - if telegram_ips_cache: + if telegram_ips_cache := cache.get("telegram_safe_ip_list"): return telegram_ips_cache telegram_ips = requests.get("https://core.telegram.org/bots/webhooks").json()[ From fbe32deefbc8a6274f1e235aee5067a7ac54cc23 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:34:20 +0330 Subject: [PATCH 096/110] fixed code quality issues --- telegram/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/telegram/views.py b/telegram/views.py index de6fbd6..f94de3c 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -83,9 +83,8 @@ def user_profile(self): def perform_create(self, serializer: TelegramConnectionSerializer): telegram_data = serializer.validated_data - is_verified = TelegramUtil().verify_login(telegram_data) - if is_verified: + if TelegramUtil().verify_login(telegram_data): user_id = telegram_data["id"] TelegramMessenger.get_instance().send_message( From e27abcd266e3ad0f15c521f76ad9b23774c1332b Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:36:04 +0330 Subject: [PATCH 097/110] added if early raise method (refactor) --- telegram/views.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/telegram/views.py b/telegram/views.py index f94de3c..caff1b4 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -48,15 +48,14 @@ def telebot_respond(request): ): raise PermissionDenied("Invalid secret token") - if request.META["CONTENT_TYPE"] == "application/json": - json_data = request.body.decode("utf-8") - update = telebot.types.Update.de_json(json_data) - telebot_instance.process_new_updates([update]) - return HttpResponse("") - - else: + if request.META["CONTENT_TYPE"] != "application/json": raise PermissionDenied + json_data = request.body.decode("utf-8") + update = telebot.types.Update.de_json(json_data) + telebot_instance.process_new_updates([update]) + return HttpResponse("") + welcome_text = """*Welcome to Unitap!* πŸŽ‰ From 5df648fd9d32db9e40c4d48f106c5dd3dce8b18f Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:37:23 +0330 Subject: [PATCH 098/110] refactored code on return simplification --- telegram/bot.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index f478ca1..1535453 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -135,10 +135,7 @@ def handle_user_message(self, message: types.Message): """ instance = self.message_handlers.get(message.text) - if not instance: - return None - - return instance.handler(message) + return None if not instance else instance.handler(message) def handle_user_command(self, message: types.Message): """ @@ -155,10 +152,7 @@ def handle_user_command(self, message: types.Message): instance = self.command_handlers.get(command) - if not instance: - return None - - return instance.handler(message, command, args[1:]) + return None if not instance else instance.handler(message, command, args[1:]) def build_callback_string(self, command: str, args: list[str]): if not args: From a9a43d090b3cf61f70bf2609f025db6472f00faa Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Thu, 17 Oct 2024 15:38:13 +0330 Subject: [PATCH 099/110] added return simplification additional to last commit --- telegram/bot.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 1535453..23bcccb 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -176,10 +176,7 @@ def handle_callback_query(self, call: types.CallbackQuery): instance = self.callback_handlers.get(cmd_parts[0]) - if not instance: - return None - - return instance.handler(call) + return None if not instance else instance.handler(call) def on_telegram_message(self, message: types.Message): """ From a4da9d4222e166c12b982517f830c9080bfe6c25 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 19 Oct 2024 12:23:46 +0330 Subject: [PATCH 100/110] mocked passport receive signal --- authentication/tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/authentication/tests.py b/authentication/tests.py index f5e5779..cc8208a 100644 --- a/authentication/tests.py +++ b/authentication/tests.py @@ -69,6 +69,11 @@ def create_verified_user() -> UserProfile: return user +@patch("authentication.models.submit_passport") +def mock_submit_passport(sender, instance: GitcoinPassportConnection, **kwargs): + return None + + def create_new_wallet(user_profile, _address, wallet_type) -> Wallet: wallet, is_create = Wallet.objects.get_or_create( user_profile=user_profile, address=_address, wallet_type=wallet_type From 4cac304a67bae987558267b7c8af0c64502fc220 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 19 Oct 2024 12:24:57 +0330 Subject: [PATCH 101/110] added import on django apps ready --- telegram/apps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telegram/apps.py b/telegram/apps.py index afe6606..2bac21d 100644 --- a/telegram/apps.py +++ b/telegram/apps.py @@ -11,6 +11,7 @@ def ready(self) -> None: return super().ready() from .bot import TelegramMessenger + from telegram import messages messenger = TelegramMessenger.get_instance() messenger.ensure_webhook() From 16b5724e0af2cb43ba3f225e48e935c7690f0cc2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 19 Oct 2024 12:41:19 +0330 Subject: [PATCH 102/110] fixed patching mock gitcoin passport decorator --- authentication/tests.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/authentication/tests.py b/authentication/tests.py index cc8208a..f300e43 100644 --- a/authentication/tests.py +++ b/authentication/tests.py @@ -69,11 +69,6 @@ def create_verified_user() -> UserProfile: return user -@patch("authentication.models.submit_passport") -def mock_submit_passport(sender, instance: GitcoinPassportConnection, **kwargs): - return None - - def create_new_wallet(user_profile, _address, wallet_type) -> Wallet: wallet, is_create = Wallet.objects.get_or_create( user_profile=user_profile, address=_address, wallet_type=wallet_type @@ -688,6 +683,7 @@ def setUp(self) -> None: user_profile=self.user_profile, _address=self.address, wallet_type="EVM" ) + @patch("authentication.models.submit_passport", lambda a, b: True) def test_gitcoin_passport_connection_successful(self): self.client.force_authenticate(user=self.user_profile.user) response = self.client.post( @@ -702,6 +698,7 @@ def test_gitcoin_passport_connection_successful(self): 1, ) + @patch("authentication.models.submit_passport", lambda a, b: True) def test_gitcoin_passport_not_exists(self): address_does_not_have_gitcoin_passport = ( "0x0cE49AF5d8c5A70Edacd7115084B2b3041fE4fF5" @@ -718,6 +715,7 @@ def test_gitcoin_passport_not_exists(self): ) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) + @patch("authentication.models.submit_passport", lambda a, b: True) def test_address_not_owned_by_user(self): self.client.force_authenticate(user=self.user_profile.user) response = self.client.post( From 70fbcfaa8a64f2c42db4565ae58d898347a08850 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sun, 20 Oct 2024 13:09:54 +0330 Subject: [PATCH 103/110] fixed testing error --- .github/workflows/django.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 86ff284..48418c4 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -56,3 +56,4 @@ jobs: CONSUMER_KEY: ${{ secrets.CONSUMER_KEY }} CONSUMER_SECRET: ${{ secrets.CONSUMER_SECRET }} DEPLOYMENT_ENV: "dev" + TELEGRAM_BOT_API_KEY: ${{ secrets.TELEGRAM_BOT_API_KEY }} From 21baa4d883662a235a6532fa5b4656f37a7ddae8 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Wed, 23 Oct 2024 11:50:52 +0330 Subject: [PATCH 104/110] changed gitcoin test address --- authentication/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authentication/tests.py b/authentication/tests.py index f300e43..35db4e4 100644 --- a/authentication/tests.py +++ b/authentication/tests.py @@ -677,7 +677,7 @@ def test_api_verify_login_signature_with_deleted_wallet(self): class TestGitcoinPassportThirdPartyConnection(APITestCase): def setUp(self) -> None: - self.address = "0x0cE49AF5d8c5A70Edacd7115084B2b3041fE4fF6" + self.address = "0x05204E317D25eb172115546297b056965bE2C74d" self.user_profile = create_new_user() create_new_wallet( user_profile=self.user_profile, _address=self.address, wallet_type="EVM" From 3d9862ef88b5c66f3c1d29df0a932cc8af1df4b0 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Wed, 23 Oct 2024 11:55:12 +0330 Subject: [PATCH 105/110] fixed gitcoin passport test address fixed loading tokentap params --- core/tests.py | 2 +- tokenTap/validators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/tests.py b/core/tests.py index 79c9447..3d2d8e4 100644 --- a/core/tests.py +++ b/core/tests.py @@ -316,7 +316,7 @@ def test_EAS_attest_constraints(self): class TestGitcoinPassportConstraint(BaseTestCase): def setUp(self): super().setUp() - self.address = "0x0cE49AF5d8c5A70Edacd7115084B2b3041fE4fF6" + self.address = "0x05204E317D25eb172115546297b056965bE2C74d" self.user_profile = self.user_profile create_new_wallet( user_profile=self.user_profile, _address=self.address, wallet_type="EVM" diff --git a/tokenTap/validators.py b/tokenTap/validators.py index 8870880..7d4d10d 100644 --- a/tokenTap/validators.py +++ b/tokenTap/validators.py @@ -52,7 +52,7 @@ def __init__( def check_user_permissions(self, raise_exception=True): try: - param_values = json.loads(self.td.constraint_params) + param_values = json.loads(self.td.constraint_params or "{}") except Exception as e: logging.error("Error parsing constraint params", e) param_values = {} From 4d19119c711dcb44c486ddae9ec56d87dedb29e2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 26 Oct 2024 17:20:40 +0330 Subject: [PATCH 106/110] removed telegram ip safelist validation --- telegram/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/telegram/views.py b/telegram/views.py index caff1b4..df8d793 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -34,13 +34,13 @@ def get_telegram_safe_ips(): @csrf_exempt def telebot_respond(request): - client_ip = request.META["REMOTE_ADDR"] + # client_ip = request.META["REMOTE_ADDR"] - telegram_ips = get_telegram_safe_ips + # telegram_ips = get_telegram_safe_ips - # Validate the request's IP address against Telegram's IP ranges - if client_ip not in telegram_ips: - raise PermissionDenied("Invalid IP address") + # # Validate the request's IP address against Telegram's IP ranges + # if client_ip not in telegram_ips: + # raise PermissionDenied("Invalid IP address") if ( request.headers.get("X-Telegram-Bot-Api-Secret-Token") From 1b7d75e7bce794dbaa43a3f3349f6a51d7f16f4f Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 26 Oct 2024 17:37:07 +0330 Subject: [PATCH 107/110] added exception handling --- telegram/bot.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 23bcccb..db397a6 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -1,18 +1,51 @@ from authentication.models import UserProfile from django.conf import settings +from django.utils import timezone from abc import ABC, abstractmethod -from telebot import types +from telebot import types, ExceptionHandler import telebot import time import logging +import traceback from telegram.models import TelegramConnection logger = logging.getLogger(__name__) -telebot_instance = telebot.TeleBot(settings.TELEGRAM_BOT_API_KEY) + +class ExceptionHandler(ExceptionHandler): + def handle(self, exception: Exception): + # Timestamp for when the exception occurred + timestamp = timezone.now().strftime("%d/%m/%Y, %H:%M:%S") + + logger.error(f"Custom exception handler triggered for {exception}: {timestamp}") + traceback.print_exception(type(exception), exception, exception.__traceback__) + + # Detailed message with improved readability + exception_message = ( + f"**Exception Details**\n\n" + f"**Exception Type:** `{exception.__class__.__name__}`\n" + f"**Occurred At:** `{timestamp}`\n" + f"\n**Traceback:**\n" + f"```\n{''.join(traceback.format_exception(type(exception), exception, exception.__traceback__))}\n```" + ) + + sticker_emoji = "πŸͺ²πŸͺ²" # Customize as needed for better visibility + TelegramMessenger.get_instance().send_message( + chat_id=settings.TELEGRAM_BUG_REPORTER_CHANNEL_ID, + text=( + f"{sticker_emoji} **An error has occurred!** {sticker_emoji}\n\n" + f"{exception_message}" + ), + ) + + +telebot_instance = telebot.TeleBot( + settings.TELEGRAM_BOT_API_KEY, exception_handler=ExceptionHandler() +) + MAX_REQUESTS_PER_SECOND = 30 From 5e3dabf30faca4c0000559ef9cf89735f1b20f30 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 26 Oct 2024 17:46:44 +0330 Subject: [PATCH 108/110] added logs for debugging --- telegram/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telegram/views.py b/telegram/views.py index df8d793..048cd74 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -17,6 +17,10 @@ import telebot import requests +import logging + + +logger = logging.getLogger(__name__) def get_telegram_safe_ips(): @@ -42,6 +46,8 @@ def telebot_respond(request): # if client_ip not in telegram_ips: # raise PermissionDenied("Invalid IP address") + logger.info(request.headers) + if ( request.headers.get("X-Telegram-Bot-Api-Secret-Token") != settings.TELEGRAM_BOT_API_SECRET From 58310346a113e4b0b70a1e0e3dff444e67db346f Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 26 Oct 2024 19:17:57 +0330 Subject: [PATCH 109/110] fixed telegram respond issue --- telegram/bot.py | 15 +++++++++------ telegram/views.py | 2 -- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index db397a6..d8b2b2b 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -233,14 +233,13 @@ def ensure_webhook(self): """ import requests - # Replace with your webhook and Telegram Bot token webhook_url = "https://api.unitap.app/api/telegram/wh/" telegram_api_url = ( f"https://api.telegram.org/bot{telebot_instance.token}/setWebhook" ) # Register webhook with secret token for added security - requests.post( + res = requests.post( telegram_api_url, data={"url": webhook_url, "secret_token": settings.TELEGRAM_BOT_API_SECRET}, ) @@ -250,6 +249,10 @@ def ready(self): Prepare the bot by registering the message and callback handlers for processing updates. This is the setup function that connects Telegram message updates with handler functions. """ + self.callback_handlers = register_callback_handlers() + self.message_handlers = register_message_handlers() + self.command_handlers = register_command_handlers() + telebot_instance.message_handler(func=lambda _: True)( lambda message: self.on_telegram_message(message) ) @@ -387,9 +390,9 @@ def handler(self, callback: types.CallbackQuery): def register_message_handlers(): return { - subclass.callback: subclass(telebot_instance) + subclass.message: subclass(telebot_instance) for subclass in BaseTelegramMessageHandler.__subclasses__() - if subclass.callback + if subclass.message } @@ -403,7 +406,7 @@ def register_callback_handlers(): def register_command_handlers(): return { - subclass.callback: subclass(telebot_instance) + subclass.command: subclass(telebot_instance) for subclass in BaseTelegramCommandHandler.__subclasses__() - if subclass.callback + if subclass.command } diff --git a/telegram/views.py b/telegram/views.py index 048cd74..d0721c0 100644 --- a/telegram/views.py +++ b/telegram/views.py @@ -46,8 +46,6 @@ def telebot_respond(request): # if client_ip not in telegram_ips: # raise PermissionDenied("Invalid IP address") - logger.info(request.headers) - if ( request.headers.get("X-Telegram-Bot-Api-Secret-Token") != settings.TELEGRAM_BOT_API_SECRET From f1e49d010cb815647c5dcf3f581521533daf36b2 Mon Sep 17 00:00:00 2001 From: Ali Maktabi Date: Sat, 26 Oct 2024 19:28:13 +0330 Subject: [PATCH 110/110] fixed texts error --- telegram/messages/about.py | 8 ++++---- telegram/messages/gastap_stats.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/telegram/messages/about.py b/telegram/messages/about.py index 7c2febc..d36cce3 100644 --- a/telegram/messages/about.py +++ b/telegram/messages/about.py @@ -7,10 +7,10 @@ Welcome to Unitap, your smart companion for managing tasks, getting updates, and automating processes! Whether you're working on a project, organizing events, or just need help staying on top of things, Unitap is here to assist. With Unitap, you can: -- **Receive timely notifications** for important events. -- **Submit and track issues** directly within your workspace. -- **Connect with services** and streamline your workflow. -- **Ask for help or request hints** to navigate challenges. +\- **Receive timely notifications** for important events. +\- **Submit and track issues** directly within your workspace. +\- **Connect with services** and streamline your workflow. +\- **Ask for help or request hints** to navigate challenges. Unitap is designed to integrate seamlessly with your tools, making your work life smoother and more efficient. Start interacting today by typing `/help` to see available commands! diff --git a/telegram/messages/gastap_stats.py b/telegram/messages/gastap_stats.py index 123998a..2918813 100644 --- a/telegram/messages/gastap_stats.py +++ b/telegram/messages/gastap_stats.py @@ -11,7 +11,7 @@ {% for faucet in faucets %} πŸ”΅ *{{ faucet.chain.chain_name }}* Max Claim Amount: {{ faucet.max_claim_amount }} -Available for: {% if faucet.is_one_time_claim %}One-time{% else %}Weekly{% endif %} +Available for: {% if faucet.is_one_time_claim %}One Time{% else %}Weekly{% endif %} {% if faucet.has_enough_funds %} 🟒 *Status:* Available @@ -20,7 +20,6 @@ πŸ”΄ *Status:* Out of Balance {% endif %} ---- {% endfor %} Remember: You need BrightID verification to claim your tokens.